Create type-safe API endpoints with Encore.ts.
When creating API endpoints with Encore.ts, follow these patterns:
import { api } from "encore.dev/api";
Always define explicit TypeScript interfaces for request and response types:
interface CreateUserRequest {
email: string;
name: string;
}
interface CreateUserResponse {
id: string;
email: string;
name: string;
}
export const createUser = api(
{ method: "POST", path: "/users", expose: true },
async (req: CreateUserRequest): Promise<CreateUserResponse> => {
// Implementation
}
);
| Option | Type | Description |
|--------|------|-------------|
| method | string | HTTP method: GET, POST, PUT, PATCH, DELETE |
| path | string | URL path, supports :param and *wildcard |
| expose | boolean | If true, accessible from outside (default: false) |
| auth | boolean | If true, requires authentication |
| sensitive | boolean | If true, redacts request/response payloads from traces |
Encore supports four endpoint configurations:
// Both request and response
export const createUser = api(
{ method: "POST", path: "/users", expose: true },
async (req: CreateRequest): Promise<CreateResponse> => { ... }
);
// Response only (no request body)
export const listUsers = api(
{ method: "GET", path: "/users", expose: true },
async (): Promise<ListResponse> => { ... }
);
// Request only (no response body)
export const deleteUser = api(
{ method: "DELETE", path: "/users/:id", expose: true },
async (req: DeleteRequest): Promise<void> => { ... }
);
// Neither request nor response
export const ping = api(
{ method: "GET", path: "/ping", expose: true },
async (): Promise<void> => { ... }
);
Include an HttpStatus field in your response to return custom status codes:
import { api, HttpStatus } from "encore.dev/api";
interface CreateResponse {
id: string;
status: HttpStatus;
}
export const create = api(
{ method: "POST", path: "/items", expose: true },
async (req: CreateRequest): Promise<CreateResponse> => {
const item = await createItem(req);
return { id: item.id, status: HttpStatus.Created }; // Returns 201
}
);
// Path: "/users/:id"
interface GetUserRequest {
id: string; // Automatically mapped from :id
}
import { Query } from "encore.dev/api";
interface ListUsersRequest {
limit?: Query<number>;
offset?: Query<number>;
}
import { Header } from "encore.dev/api";
interface WebhookRequest {
signature: Header<"X-Webhook-Signature">;
payload: string;
}
import { Cookie } from "encore.dev/api";
interface SessionRequest {
session?: Cookie<"session">;
settings?: Cookie<"user-settings">;
}
Encore validates requests at runtime using TypeScript types. Add constraints for stricter validation:
import { api } from "encore.dev/api";
import { Min, Max, MinLen, MaxLen, IsEmail, IsURL } from "encore.dev/validate";
interface CreateUserRequest {
email: string & IsEmail; // Must be valid email
username: string & MinLen<3> & MaxLen<20>; // 3-20 characters
age: number & Min<13> & Max<120>; // Between 13 and 120
website?: string & IsURL; // Optional, must be URL if provided
}
Use & for AND logic (must pass all rules) and | for OR logic (must pass at least one):
import { IsEmail, IsURL, MinLen, MaxLen } from "encore.dev/validate";
interface ContactRequest {
// Must be valid email OR valid URL
contact: string & (IsEmail | IsURL);
// Must be 5-100 chars AND be a valid URL
website: string & MinLen<5> & MaxLen<100> & IsURL;
}
| Validator | Applies To | Example |
|-----------|-----------|---------|
| Min<N> | number | age: number & Min<18> |
| Max<N> | number | count: number & Max<100> |
| MinLen<N> | string, array | name: string & MinLen<1> |
| MaxLen<N> | string, array | tags: string[] & MaxLen<10> |
| IsEmail | string | email: string & IsEmail |
| IsURL | string | link: string & IsURL |
| StartsWith<S> | string | id: string & StartsWith<"usr_"> |
| EndsWith<S> | string | file: string & EndsWith<".json"> |
| MatchesRegexp<R> | string | code: string & MatchesRegexp<"^[A-Z]{3}$"> |
Invalid requests return 400 with details:
{
"code": "invalid_argument",
"message": "validation failed",
"details": { "field": "email", "error": "must be a valid email" }
}
Use APIError for proper HTTP error responses:
import { APIError, ErrCode } from "encore.dev/api";
// Throw with error code
throw new APIError(ErrCode.NotFound, "user not found");
// Or use shorthand
throw APIError.notFound("user not found");
throw APIError.invalidArgument("email is required");
throw APIError.unauthenticated("invalid token");
| Code | HTTP Status | Usage |
|------|-------------|-------|
| NotFound | 404 | Resource doesn't exist |
| InvalidArgument | 400 | Bad input |
| Unauthenticated | 401 | Missing/invalid auth |
| PermissionDenied | 403 | Not allowed |
| AlreadyExists | 409 | Duplicate resource |
Serve static files (HTML, CSS, JS, images) with api.static:
import { api } from "encore.dev/api";
// Serve files from ./assets under /static/*
export const assets = api.static(
{ expose: true, path: "/static/*path", dir: "./assets" }
);
// Serve at root (use !path for fallback routing)
export const frontend = api.static(
{ expose: true, path: "/!path", dir: "./dist" }
);
// Custom 404 page
export const app = api.static(
{ expose: true, path: "/!path", dir: "./public", notFound: "./404.html" }
);
*path - Standard wildcard: matches all paths under the prefix (e.g., /static/*path)!path - Fallback routing: serves static files at domain root without conflicting with other API endpoints. Use this for SPAs where unmatched routes should serve index.htmlimport not requireexpose: true only for public endpointsAPIError instead of returning error objectsapi.raw — see the encore-webhook skillMin, MaxLen, etc.) for user inputSearch for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer