Fastify is a Node.js framework built around schema-based validation and a plugin architecture, designed for developers who want explicit control over their HTTP layer. Encore.ts takes a different approach, focusing on distributed systems with automatic infrastructure provisioning while also delivering high performance through its Rust-based runtime.
Both frameworks support TypeScript and prioritize developer experience. The choice depends on whether you need maximum control over your HTTP layer or want integrated infrastructure automation.
| Aspect | Fastify | Encore.ts |
|---|---|---|
| Philosophy | Schema-validated HTTP framework | Infrastructure-aware application framework |
| Performance | Good | Excellent (Rust-based runtime) |
| Type Safety | JSON Schema + TypeBox | Native TypeScript types |
| Validation | JSON Schema (compile-time code generation) | TypeScript types (compile-time validation) |
| Local Infrastructure | Configure yourself | Automatic (databases, Pub/Sub, cron) |
| Plugin System | Extensive ecosystem | Infrastructure primitives |
| Observability | Manual setup | Built-in tracing, metrics, logs |
| AI Agent Compatibility | Manual configuration needed | Built-in infrastructure awareness |
| Best For | Schema-validated APIs, plugin architecture | Distributed systems, full-stack automation |
Let's start with a simple REST endpoint.
import Fastify from 'fastify';
const fastify = Fastify({ logger: true });
fastify.get('/hello/:name', async (request, reply) => {
const { name } = request.params as { name: string };
return { message: `Hello, ${name}!` };
});
fastify.listen({ port: 3000 });
Fastify is explicit about server setup. You create the instance, define routes, and start the server. The framework stays out of your way while providing excellent defaults.
import { api } from "encore.dev/api";
interface HelloResponse {
message: string;
}
export const hello = api(
{ method: "GET", path: "/hello/:name", expose: true },
async ({ name }: { name: string }): Promise<HelloResponse> => {
return { message: `Hello, ${name}!` };
}
);
Encore uses a declarative approach. You define the endpoint with its types, and Encore handles server setup, routing, and request validation.
Verdict: Encore is more concise and handles server setup, routing, and validation automatically.
Both frameworks take validation seriously, but with different approaches.
Fastify uses JSON Schema for validation, with TypeBox providing TypeScript integration:
import Fastify from 'fastify';
import { Type, Static } from '@sinclair/typebox';
const fastify = Fastify();
const CreateUserSchema = Type.Object({
email: Type.String({ format: 'email' }),
name: Type.String({ minLength: 1 }),
});
type CreateUserBody = Static<typeof CreateUserSchema>;
const CreateUserResponseSchema = Type.Object({
id: Type.Number(),
email: Type.String(),
name: Type.String(),
});
fastify.post<{ Body: CreateUserBody }>(
'/users',
{
schema: {
body: CreateUserSchema,
response: { 200: CreateUserResponseSchema },
},
},
async (request, reply) => {
const { email, name } = request.body;
return { id: 1, email, name };
}
);
Fastify compiles JSON schemas to validation functions at startup. TypeBox bridges the gap between JSON Schema and TypeScript types, but you're maintaining two representations.
import { api } from "encore.dev/api";
interface CreateUserRequest {
email: string;
name: string;
}
interface User {
id: number;
email: string;
name: string;
}
export const createUser = api(
{ method: "POST", path: "/users", expose: true },
async (req: CreateUserRequest): Promise<User> => {
return { id: 1, email: req.email, name: req.name };
}
);
Encore validates requests based on TypeScript types directly. No separate schema definition. For additional constraints, use built-in validation rules:
import { MinLen, IsEmail } from "encore.dev/validate";
interface CreateUserRequest {
email: string & IsEmail;
name: string & MinLen<1>;
}
Verdict: Encore's approach is more concise—types serve as the single source of truth. Fastify requires maintaining separate JSON schemas alongside TypeScript types.
Fastify was historically positioned as one of the fastest Node.js frameworks. However, recent benchmarks show this advantage has diminished as Node.js performance has improved overall.
Fastify's architecture includes:
fast-json-stringify// Fastify compiles schemas for serialization
fastify.get('/users/:id', {
schema: {
response: {
200: {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
},
},
},
},
handler: async (request, reply) => {
return { id: 1, name: 'John' };
},
});
Encore.ts takes a different approach with a Rust-based runtime that handles HTTP parsing, routing, and validation outside the Node.js event loop:
export const getUser = api(
{ method: "GET", path: "/users/:id", expose: true },
async ({ id }: { id: number }): Promise<User> => {
// The Rust runtime handles HTTP, routing, and validation
// Your TypeScript code focuses on business logic
return { id, name: "John" };
}
);
This architecture offloads performance-critical work to compiled code while keeping your application logic in TypeScript. In benchmarks, Encore.ts significantly outperforms both Fastify and Express in request throughput.
Verdict: Performance is no longer Fastify's primary differentiator. If raw throughput matters, Encore's Rust-based runtime has a clear advantage. For most applications, choose based on other factors like infrastructure needs and developer experience.
Fastify has an extensive plugin ecosystem:
import Fastify from 'fastify';
import fastifyPostgres from '@fastify/postgres';
import fastifyRedis from '@fastify/redis';
import fastifyRateLimit from '@fastify/rate-limit';
const fastify = Fastify();
// Register plugins
await fastify.register(fastifyPostgres, {
connectionString: process.env.DATABASE_URL,
});
await fastify.register(fastifyRedis, {
host: process.env.REDIS_HOST,
});
await fastify.register(fastifyRateLimit, {
max: 100,
timeWindow: '1 minute',
});
fastify.get('/users/:id', async (request, reply) => {
const { rows } = await fastify.pg.query(
'SELECT * FROM users WHERE id = $1',
[request.params.id]
);
return rows[0];
});
Plugins integrate with Fastify's lifecycle and encapsulation system. You configure connections, manage credentials, and handle local development setup yourself.
Encore provides infrastructure as code primitives:
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { CronJob } from "encore.dev/cron";
import { Topic, Subscription } from "encore.dev/pubsub";
const db = new SQLDatabase("users", { migrations: "./migrations" });
const userCreated = new Topic<{ userId: string }>("user-created", {
deliveryGuarantee: "at-least-once",
});
const _ = new CronJob("cleanup", {
title: "Clean up old records",
every: "24h",
endpoint: cleanupOldRecords,
});
export const getUser = api(
{ method: "GET", path: "/users/:id", expose: true },
async ({ id }: { id: string }) => {
return await db.queryRow`SELECT * FROM users WHERE id = ${id}`;
}
);
Databases, Pub/Sub, and cron jobs are provisioned automatically in local development. No Docker setup, no connection strings, no environment variables.
Verdict: Encore provides integrated infrastructure with automatic provisioning. Fastify requires manual setup for databases, connection strings, and local development.
Local development with Fastify typically requires:
# Start PostgreSQL
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=secret postgres
# Set environment variables
export DATABASE_URL=postgres://postgres:secret@localhost:5432/myapp
# Run migrations
npm run migrate
# Start the server
npm run dev
You manage your own database instances, connection strings, and supporting services.
encore run
Encore provisions local PostgreSQL databases, runs migrations, and starts your services. The local development dashboard at localhost:9400 provides API testing, database inspection, and request tracing.

Verdict: Encore automates local infrastructure, which speeds up development significantly. Fastify requires manual database setup with Docker and environment variables.
Fastify supports observability through plugins and manual instrumentation:
import Fastify from 'fastify';
import { trace } from '@opentelemetry/api';
const fastify = Fastify({ logger: true });
// Manual tracing
fastify.addHook('onRequest', async (request) => {
const tracer = trace.getTracer('my-service');
request.span = tracer.startSpan(`${request.method} ${request.url}`);
});
fastify.addHook('onResponse', async (request) => {
request.span?.end();
});
You configure logging, tracing exporters, and metrics collection. Fastify's built-in logger is excellent, but distributed tracing requires setup.
import { api } from "encore.dev/api";
import log from "encore.dev/log";
export const getUser = api(
{ method: "GET", path: "/users/:id", expose: true },
async ({ id }: { id: string }) => {
log.info("fetching user", { userId: id });
// All database queries and service calls are traced automatically
return { id, name: "John" };
}
);
Every request is traced end-to-end. Database queries, service calls, and Pub/Sub messages appear in traces automatically.

Verdict: Encore provides built-in observability without configuration. Fastify requires manual setup with OpenTelemetry and external services.
Building microservices with Fastify means handling service communication yourself:
// users-service
const fastify = Fastify();
fastify.get('/users/:id', async (request) => {
return { id: request.params.id, name: 'John' };
});
fastify.listen({ port: 3001 });
// orders-service
const fastify = Fastify();
fastify.post('/orders', async (request) => {
// Manual service call
const response = await fetch(
`${process.env.USERS_SERVICE_URL}/users/${request.body.userId}`
);
const user = await response.json();
return { orderId: 1, user };
});
fastify.listen({ port: 3002 });
You manage URLs, handle network errors, implement retries, and correlate traces manually.
// users/api.ts
export const getUser = api(
{ method: "GET", path: "/users/:id", expose: true },
async ({ id }: { id: string }) => {
return { id, name: "John" };
}
);
// orders/api.ts
import { users } from "~encore/clients";
export const createOrder = api(
{ method: "POST", path: "/orders", expose: true },
async (req: CreateOrderRequest) => {
// Type-safe call, automatic service discovery
const user = await users.getUser({ id: req.userId });
return { orderId: 1, user };
}
);
Service calls look like function calls. Encore handles service discovery and correlates traces automatically. The service catalog shows how services communicate.

Verdict: Fastify is flexible for microservices but requires more infrastructure work. Encore makes microservices feel like a monolith to develop, with type-safe calls and automatic service discovery.
Fastify makes sense when:
Encore.ts makes sense when:
Try both with a small project:
You might also find our Express.js vs Encore.ts comparison helpful, or check out the Best TypeScript Backend Frameworks guide for a broader perspective.
Have questions about choosing a framework? Join our Discord community where developers discuss architecture decisions daily.