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 theexpose
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.tsimport { 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.