Defining Type-Safe APIs

Simplifying type-safe API development

Encore.ts simplifies creating type-safe, idiomatic TypeScript API endpoints and provides built-in validation for incoming requests.

At their core, APIs in Encore.ts are normal async functions with request and response data types defined as TypeScript interfaces, which Encore.ts uses to encode API requests to HTTP messages.

Encore.ts also parses your source code to understand the request and response schema of each endpoint, automatically handling validation of incoming requests against your schema.

Defining API endpoints

To define an API endpoint, use the api function from the encore.dev/api module. This function wraps a regular TypeScript async function, designating it as an API endpoint. Encore.ts then generates the necessary boilerplate at compile-time.

In the example below, we define the API endpoint ping which accepts POST requests and is exposed as hello.ping (because our service name is hello).

// inside the hello.ts file import { api } from "encore.dev/api"; export const ping = api( { method: "POST" }, async (p: PingParams): Promise<PingResponse> => { return { message: `Hello ${p.name}!` }; }, );

Exposing API endpoints to the outside world

When you define an API, by default it is not exposed to the outside world, and it can only be called by other APIs within the same Encore application.

To expose an API to the internet, add the expose: true field to the options object passed in as the first argument to api.

  • { expose: false } defines a private API that is never accessible to the outside world. It can only be called from other services in your app and via cron jobs. This is default value if the expose field isn't set.
  • { expose: true } defines a public API that anybody on the internet can call

Requiring authentication data

To require authentication for an API endpoint, add auth: true to the API options. With this option, Encore will first call the authentication handler you've defined to validate the authentication of incoming requests.

Setting auth: true can also be useful for internal APIs that aren't exposed to the internet. In that case, it means that the internal caller must have valid authentication data associated with its request.

Finally, even if an API endpoint does not specify auth: true, it will still receive any authentication data that was provided.

For more information on defining APIs that require authentication, see the authentication guide.

API Schemas

Request and response schemas

In the example above we defined an API that uses request and response schemas, where the request data is of type Params and the response data of type Response. That means we need to define them like so:

hello.ts
import { api } from "encore.dev/api"; // PingParams is the request data for the Ping endpoint. interface PingParams { name: string; } // PingResponse is the response data for the Ping endpoint. interface PingResponse { message: string; } // hello is an API endpoint that responds with a simple response. export const hello = api( { method: "POST", path: "/hello" }, async (p: PingParams): Promise<PingResponse> => { return { message: `Hello ${p.name}!` }; }, );

Request and response schemas are both optional. There are four different ways of defining an API:

Using both request and response data:
api({ ... }, async (params: Params): Promise<Response> => {});

Only returning a response:
api({ ... }, async (): Promise<Response> => {});

With only request data:
api({ ... }, async (params: Params): Promise<void> => {});

Without any request or response data:
api({ ... }, async (): Promise<void> => {});

Alternatively, you can express these using type parameters, since api is a generic function:

Using both request and response data:
api<Params, Response>({ ... }, async (params) => {});

Only returning a response:
api<void, Response>({ ... }, async () => {});

With only request data:
api<Params, void>({ ... }, async (params) => {});

Without any request or response data:
api<void, void>({ ... }, async () => {});

Customizing request and response encoding

Encore parses the source code to understand the request and response schema of each endpoint. By default, the data is parsed as a JSON body for incoming requests, and written back as JSON responses.

This can be customized on a per-field basis, allowing individual fields to be parsed from query strings and HTTP headers with ease.

This is done by using the Header and Query types defined in the encore.dev/api module.

Headers

Headers are defined by setting the field type to Header<"Name-Of-Header">. It can be used in both request and response data types.

In the example below, the language field will be fetched from the Accept-Language HTTP header.

import { Header } from "encore.dev/api"; interface Params { language: Header<"Accept-Language">; // parsed from header author: string; // not a header }

Query parameters

For GET, HEAD and DELETE requests, parameters are read from the query string by default, since those HTTP methods do not support request bodies.

For other HTTP methods (that do support request bodies), parameters are by default read from the HTTP request body as JSON. In those cases, the Query type can be used to specify that a field should be parsed from the query string instead.

Query strings are not supported in HTTP responses, and are treated as being part of the HTTP response body in JSON.

In the example below, the limit field will be read from the limit query parameter for all HTTP methods, whereas the author field will be parsed from the query string only if the method of the request is GET, HEAD or DELETE (and otherwise from the HTTP request body as JSON).

interface Params { limit: Query<number>; // always a query parameter author: string; // query if GET, HEAD or DELETE, otherwise body parameter }

Path parameters

Path parameters are specified by the path field in the API Options in api call. To specify a placeholder variable, use :name and add a function parameter with the same name to the function signature. Encore parses the incoming request URL and makes sure it matches the type of the parameter. The last segment of the path can be parsed as a wildcard parameter by using *name with a matching function parameter.

Each path parameter (whether a single segment like :name or a wildcard parameter like *name) must have a matching field in the request data type.

For example:

// Retrieves a blog post by its id. export const getBlogPost = api( {method: "GET", path: "/blog/:id/*path"}, async (params: {id: number; path: string}): Promise<BlogPost> { // Use id and path to query database... } )

Raw endpoints

In case you need to operate at a lower abstraction level, Encore supports defining raw endpoints that let you access the underlying HTTP request. This is often useful for things like accepting webhooks. Learn more in the raw endpoints guide.