Encore: The TypeScript Backend Framework with Built-In Observability
Most backend frameworks give you routing and middleware. You still need to separately provision databases, set up Pub/Sub, configure caching, set up tracing, and write deployment configuration. Encore takes a different approach: you declare what infrastructure your code needs, and Encore handles the rest — locally and in production.
It's a TypeScript framework where infrastructure is defined inline with your code. A Pub/Sub topic is a typed TypeScript object. A database is declared at the top of your service file. Encore reads these declarations and provisions the infrastructure automatically.
What Makes Encore Different
Infrastructure as code, co-located with application code:
import { SQLDatabase } from "encore.dev/storage/sqldb";
const db = new SQLDatabase("orders", {
migrations: "./migrations",
});
This isn't configuration. It's TypeScript that runs locally with a local database automatically provisioned, and deploys to a real database in production.
Automatic service mesh and tracing: Define multiple services, and Encore automatically creates a distributed tracing setup. Service-to-service calls are traced, latency is measured, and a service catalog is generated — without configuring Jaeger, Zipkin, or any tracing infrastructure.
Type-safe service-to-service calls: Call another service like a function:
// From service A, call service B's API
const result = await orders.getOrder({ id: "order123" });
// Fully typed — no HTTP client configuration needed
Installation and Setup
# Install Encore CLI
curl -L https://encore.dev/install.sh | bash
# Create a new app
encore app create my-app
cd my-app
# Start the local development environment
encore run
The encore run command starts your app with a local Postgres database, the Encore Dashboard (tracing UI), and a development server.
Defining APIs
// orders/orders.ts
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
const db = new SQLDatabase("orders", {
migrations: "./migrations",
});
interface Order {
id: string;
userId: string;
total: number;
status: "pending" | "completed" | "cancelled";
}
interface CreateOrderRequest {
userId: string;
items: Array<{ productId: string; quantity: number; price: number }>;
}
export const createOrder = api(
{ expose: true, method: "POST", path: "/orders" },
async (req: CreateOrderRequest): Promise<Order> => {
const id = crypto.randomUUID();
const total = req.items.reduce((sum, item) => sum + item.quantity * item.price, 0);
await db.exec`
INSERT INTO orders (id, user_id, total, status)
VALUES (${id}, ${req.userId}, ${total}, 'pending')
`;
return { id, userId: req.userId, total, status: "pending" };
}
);
export const getOrder = api(
{ expose: true, method: "GET", path: "/orders/:id" },
async ({ id }: { id: string }): Promise<Order> => {
const order = await db.queryRow<Order>`
SELECT id, user_id as "userId", total, status
FROM orders WHERE id = ${id}
`;
if (!order) throw new Error("Order not found");
return order;
}
);
The api() function declares an HTTP endpoint. The request/response types are TypeScript interfaces — no schema files, no code generation.
Database Migrations
Create SQL migration files in ./migrations:
-- orders/migrations/1_create_orders.up.sql
CREATE TABLE orders (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
total DECIMAL(10,2) NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Encore runs these migrations automatically against the local database during development and against production databases during deployment.
Pub/Sub
Define topics and subscriptions:
import { Topic, Subscription } from "encore.dev/pubsub";
interface OrderCreatedEvent {
orderId: string;
userId: string;
total: number;
}
// In orders service: define the topic
export const orderCreatedTopic = new Topic<OrderCreatedEvent>("order-created", {
deliveryGuarantee: "at-least-once",
});
// Publish an event
await orderCreatedTopic.publish({ orderId, userId, total });
// In notifications service: subscribe
import { Subscription } from "encore.dev/pubsub";
import { orderCreatedTopic } from "~encore/clients";
const _ = new Subscription(orderCreatedTopic, "send-confirmation", {
handler: async (event: OrderCreatedEvent) => {
await sendConfirmationEmail(event.userId, event.orderId);
},
});
Locally, Pub/Sub uses an in-memory message broker. In production, Encore provisions AWS SQS/SNS, GCP Pub/Sub, or similar.
Caching
import { CacheCluster, CacheKeyspace } from "encore.dev/storage/cache";
const cluster = new CacheCluster("my-cache", {
eviction: "allkeys-lru",
});
interface UserProfile {
name: string;
email: string;
}
const userCache = new CacheKeyspace<UserProfile>(cluster, {
path: "users/:userId",
defaultTTL: "15m",
});
// Use it
const profile = await userCache.get({ userId: "123" });
await userCache.set({ userId: "123" }, { name: "Alice", email: "[email protected]" });
Authentication
Encore has a built-in auth handler pattern:
// auth/auth.ts
import { Gateway, Header } from "encore.dev/api";
import { authHandler } from "encore.dev/auth";
interface AuthParams {
token: Header<"Authorization">;
}
interface AuthData {
userId: string;
email: string;
}
export const myAuthHandler = authHandler<AuthParams, AuthData>(
async (params): Promise<AuthData> => {
const token = params.token.replace("Bearer ", "");
const payload = await verifyJWT(token);
return { userId: payload.sub, email: payload.email };
}
);
export const gateway = new Gateway({ authHandler: myAuthHandler });
Mark endpoints that require authentication:
export const getOrder = api(
{ auth: true, method: "GET", path: "/orders/:id" },
async (req): Promise<Order> => {
const { userId } = getAuthData(); // typed AuthData
// ...
}
);
The Encore Dashboard
When running locally, navigate to http://localhost:9400 for the Encore Dashboard:
- Service catalog: All services and their APIs with documentation
- Traces: Full distributed traces for every request
- Logs: Correlated logs per request
- API Explorer: Test your APIs with a built-in client
This is free infrastructure you'd normally spend days setting up separately.
Deployment
Encore supports several deployment options:
Encore Cloud: Connect your GitHub repo, and Encore deploys automatically. Infrastructure is provisioned in their managed environment. Good for getting started.
AWS/GCP/Azure: Encore generates Terraform and Helm charts for your infrastructure. Deploy to your own cloud account.
Docker: Encore can build Docker images for self-hosting.
# Preview what will be deployed
encore build docker my-app
# Deploy to Encore Cloud
encore deploy --env=staging
Encore vs. NestJS vs. Hono
| Aspect | Encore | NestJS | Hono |
|---|---|---|---|
| Infrastructure management | Built-in | Manual | Manual |
| Observability | Built-in | Plugin/manual | Manual |
| Boilerplate | Low | High | Low |
| Learning curve | Moderate | High | Low |
| Service mesh | Automatic | Manual | Manual |
| Runtime | Any Node.js | Node.js | Any JS runtime |
| Flexibility | Some constraints | Full | Full |
Encore's value comes from the infrastructure management and built-in observability — these are genuinely hard problems that Encore makes trivial. The tradeoff is that you're somewhat constrained to Encore's patterns.
For applications that benefit from multiple services, Pub/Sub, and caching with minimal ops overhead, Encore is compelling. For simple APIs or maximum flexibility, Hono or Express is lighter.
The Encore documentation is at encore.dev/docs and the repository is encoredev/encore.