Services and APIs

Simplifying (micro-)service development

Encore makes it simple to build applications with one or many services, without needing to manually handle the typical complexity of developing microservices.

Defining a service

With Encore you define a service by creating a folder and inside that folder defining one or more APIs within a regular TypeScript file. Encore recognizes this as a service, and uses the folder name as the service name. When deploying, Encore will automatically provision the required infrastructure for each service.

On disk it might look like this:

/my-app ├── encore.app // ... and other top-level project files ├── package.json │ ├── hello // hello service (a folder) │   ├── hello.ts // hello service code │   └── hello_test.ts // tests for hello service │ └── world // world service (a folder) └── world.ts // world service code

This means building a microservices architecture is as simple as creating multiple directories within your application.

Defining APIs

Encore allows you to easily define type-safe, idiomatic TypeScript API endpoints.

It's easy to accept both the URL path parameters, as well as JSON request body data, HTTP headers, and query strings.

It's all done in a way that is fully declarative, enabling Encore to automatically parse and validate the incoming request and ensure it matches the schema, with zero boilerplate.

To define an API, use the api function from the encore.dev/api module to wrap a regular TypeScript async function that receives the request data as input and returns response data. This tells Encore that the function is an API endpoint. Encore will then automatically generate 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}!` }; }, );

Request and response schemas

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

// inside the hello.ts file 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. // This is exposed as "hello.ping". 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 in case you don't need them. That means there are four different ways of defining an API:

  • api({ ... }, async (params: Params): Promise<Response> => {}); when you need both.
  • api({ ... }, async (): Promise<Response> => {}); when you only return a response.
  • api({ ... }, async (params: Params): Promise<void> => {}); when you only respond with success/fail.
  • api({ ... }, async (): Promise<void> => {}); when you need neither request nor response data.

The api function is a generic function.

You can also pass the type arguments for the request and response objects to the api function which looks like this: api<Params, Response>(async (params) => {});

This approach is simple but very powerful. It lets Encore use static analysis to understand the request and response schemas of all your APIs, which enables Encore to automatically generate API documentation, type-safe API clients, and much more.

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 APIOptions 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 (this is the default value if no access field is set).

Requiring authentication data

Additionally, you can specify that an API can only be called with valid authentication, by specifying the option auth: true. 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.

REST APIs

Encore has support for RESTful APIs and lets you easily define resource-oriented API URLs, parse parameters out of them, and more.

To create a REST API, start by defining an endpoint and specify the method and path fields in the APIOptions object.

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.

For example, if you want to have a getBlogPost endpoint that takes a numeric id as a parameter:

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

You can also combine path parameters with body payloads. For example, if you want to have an updateBlogPost endpoint:

interface Params { id: number; post: BlogPost; } // updateBlogPost updates an existing blog post by id. export const updateBlogPost = api( { method: "PUT", path: "/blog/:id" }, async ({ id, post }: Params): Promise<BlogPost> => { // Use id to query database... }, );

Query parameters

To define that a field should be parsed from the query string of the incoming request, wrap its type with Query<...>. For example:

import { Query } from "encore.dev/api"; interface SearchParams { filter: Query<string>; // will be parsed from "?filter=...." in the request url } interface SearchResponse { matches: BlogPost[]; // blog posts matching the search filter. } // Search for blog posts matching the filter. export const search = api<SearchParams, SearchResponse>( { path: "/blog/search" }, async ({ filter }) => { // Use filter 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.

To define a raw endpoint, use the api.raw function. It works similarly to api, but does not accept a request and response schema. Instead, it works like the Node.js http module and Express.js, where the function receives two parameters: a request object and a response writer.

It looks like this:

import { api } from "encore.dev/api"; export const myRawEndpoint = api.raw( { expose: true, path: "/raw", method: "GET" }, async (req, resp) => { resp.writeHead(200, { "Content-Type": "text/plain" }); resp.end("Hello, raw world!"); }, );

It can be called like so:

$ curl http://localhost:4000/raw
Hello, raw world!

Calling APIs

Calling an API endpoint looks like a regular function call with Encore. To call an endpoint you first need to import the service from encore.app/clients and then call the API endpoint like a regular function. When compiling your application, Encore uses static analysis to parse all APIs and make then available through the encore.app/clients module for internal calls.

In the example below, we import the service hello and call the ping endpoint using a function call to hello.ping.

import { hello } from "~encore/clients"; // import 'hello' service export const myOtherAPI = api({}, async (): Promise<void> => { const resp = await hello.ping({ name: "World" }); console.log(resp.message); // "Hello World!" });

This means your development workflow is as simple as building a monolith, even if you use multiple services. You get all the benefits of function calls, like compile-time checking of all the parameters and auto-completion in your editor, while still allowing the division of code into logical components, services, and systems.

{/ TODO: Add info about the current request meta data when available. /}