← All articles
BACKEND Encore: The TypeScript Backend Framework with Built-... 2026-03-04 · 4 min read · encore · backend · typescript

Encore: The TypeScript Backend Framework with Built-In Observability

Backend 2026-03-04 · 4 min read encore backend typescript infrastructure as code api distributed systems deployment 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:

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.