NestJS provides a lot of structure. Modules, controllers, services, DTOs, guards, interceptors, pipes, decorators on everything. For some teams, that structure is exactly what they need. For others, the ceremony gets in the way. A straightforward CRUD endpoint shouldn't require five files and a module registration.
If you've been working with NestJS and the boilerplate is slowing you down, or if you're evaluating it for a new project and the learning curve feels steep, this guide covers the most practical alternatives. Each one takes a different approach to the same problem: building TypeScript backends with less friction.
Here's how these frameworks compare across common backend requirements.
| Feature | Encore.ts | Express.js | Fastify | Hono | tRPC |
|---|---|---|---|---|---|
| Primary use case | Distributed systems | General purpose APIs | Schema-validated APIs | Edge & serverless | Type-safe API layer |
| Architecture | Services & infrastructure | Middleware chain | Plugins | Middleware chain | Procedures & routers |
| Learning Curve | Low | Low | Medium | Low | Low-Medium |
| Built-in Validation | Yes (native types) | No | Yes (JSON Schema) | Via middleware | Yes (Zod) |
| Type-safe Service Calls | Yes | No | No | No | Yes (client) |
| Database Support | Built-in (auto-provisioned) | Manual | Manual | Manual | Manual |
| Built-in Tracing | Yes | No | No | No | No |
| Infrastructure from Code | Yes | No | No | No | No |
| Dependency Injection | No (not needed) | No | No | No | No |
| Auto API Documentation | Yes | No | Yes (via plugins) | Via plugins | No |
| AI Agent Compatibility | Built-in infrastructure awareness | Manual configuration needed | Manual configuration needed | Manual configuration needed | Manual configuration needed |
Encore.ts replaces NestJS's decorator-based architecture with something more direct. Instead of controllers, services, DTOs, and module registrations, you write a function with typed parameters and Encore handles validation, routing, and documentation. Infrastructure like databases, Pub/Sub, and cron jobs are declared in TypeScript and provisioned automatically.
The reason this matters when coming from NestJS is that Encore provides the same production capabilities (structured services, type safety, observability) without the ceremony. You get multi-service architecture, but creating a new service is creating a folder, not scaffolding modules and wiring up providers.
Encore has grown to over 11,000 GitHub stars and is used in production by companies including Groupon.
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
const db = new SQLDatabase("users", { migrations: "./migrations" });
interface User {
id: number;
email: string;
name: string;
}
interface CreateUserParams {
email: string;
name: string;
}
// This is the entire endpoint. No controller, no service class,
// no DTO, no module registration.
export const createUser = api(
{ method: "POST", path: "/users", expose: true },
async (params: CreateUserParams): Promise<User> => {
return await db.queryRow<User>`
INSERT INTO users (email, name) VALUES (${params.email}, ${params.name})
RETURNING *
`;
}
);
export const getUser = api(
{ method: "GET", path: "/users/:id", expose: true },
async ({ id }: { id: number }): Promise<User> => {
return await db.queryRow<User>`SELECT * FROM users WHERE id = ${id}`;
}
);
Compare that to the NestJS equivalent, which requires a controller file, a service file, a DTO file, a module file, and often an entity file. The Encore version is a single file with the same type safety and better infrastructure support.
Encore provides the structure that draws people to NestJS without the boilerplate that drives them away. Services are organized by folder. APIs are typed functions. Infrastructure is declared and provisioned automatically. You get distributed tracing without setting up OpenTelemetry, and a service catalog that stays in sync with your code.
For teams that chose NestJS because they wanted enforced conventions and multi-service support, Encore delivers both. The difference is that Encore's conventions are lightweight. A new developer can read an Encore service in minutes because the code does what it looks like it does. There are no decorators to trace, no DI container to understand, and no module graph to navigate.

Encore uses conventions for defining services and infrastructure that keep projects consistent as they grow. If you need a specific ORM or HTTP customization, you can use standard npm libraries alongside Encore's built-in primitives. Encore runs on Node.js with a Rust-based runtime and targets production backend systems rather than edge runtimes.
Consider Encore when you want the production capabilities of NestJS (multi-service architecture, type safety, observability) without the boilerplate. It's particularly well-suited for teams building distributed systems who want infrastructure automation and type-safe service communication. It's also a strong fit if you want infrastructure-aware code that AI agents can generate and deploy with guardrails.
For a head-to-head comparison, see our detailed NestJS vs Encore.ts article.
You can get started with Encore in minutes:
curl -L https://encore.dev/install.sh | bash
encore app create my-app
cd my-app
encore run
See the Encore.ts documentation for more details, follow the REST API tutorial to build a complete application, or check out Encore AI Integration for using Encore with AI coding agents.
Express is the opposite end of the spectrum from NestJS. Where NestJS gives you too much structure, Express gives you almost none. It's a thin HTTP layer that lets you organize your code however you want. If you're leaving NestJS because you felt constrained, Express will feel liberating. If you're leaving because you wanted better built-in features, Express will feel like a step backward.
That said, Express has the largest ecosystem of any Node.js framework. If you need middleware for a niche use case, it probably exists. The framework is also the most widely understood, which makes hiring and onboarding straightforward.
import express from 'express';
import { z } from 'zod';
const app = express();
app.use(express.json());
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1),
});
app.post('/users', async (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.issues });
}
const user = await createUser(result.data);
res.status(201).json(user);
});
app.get('/users/:id', async (req, res) => {
const user = await getUserById(req.params.id);
res.json(user);
});
app.listen(3000);
Express gets out of your way. There's no module system to learn, no decorators to understand, and no DI container. You write request handlers and wire them up. For small APIs or prototypes, this means you're productive immediately.
Express was designed before TypeScript, and it shows. Request and response types are loose. Validation requires external libraries. There's no built-in schema, no auto-generated documentation, and no infrastructure tooling. For projects that need the structure NestJS provides, Express leaves you to build that structure yourself.
Consider Express when you're building a simple API, you need specific Express middleware, or your team is already experienced with Express and the project doesn't require multi-service architecture.
For a detailed comparison, see our Express.js vs Encore.ts article.
Fastify sits between Express's minimalism and NestJS's comprehensive architecture. It has opinions about validation (JSON Schema), logging (Pino), and code organization (plugins), but doesn't force a class-based architecture or dependency injection. If you want structure without the heavyweight patterns, Fastify is worth considering.
The JSON Schema approach means your validation definitions also drive serialization optimization and OpenAPI documentation. Define the schema once, and Fastify gives you runtime validation, faster serialization, and generated docs.
import Fastify from 'fastify';
const fastify = Fastify({ logger: true });
fastify.post('/users', {
schema: {
body: {
type: 'object',
required: ['email', 'name'],
properties: {
email: { type: 'string', format: 'email' },
name: { type: 'string', minLength: 1 }
}
},
response: {
201: {
type: 'object',
properties: {
id: { type: 'number' },
email: { type: 'string' },
name: { type: 'string' }
}
}
}
}
}, async (request, reply) => {
const user = await createUser(request.body);
reply.code(201);
return user;
});
fastify.listen({ port: 3000 });
Fastify provides meaningful structure without the heavyweight abstractions. The plugin system keeps code organized as the project grows. JSON Schema validation gives you a single source of truth for types, validation, and documentation. Compared to NestJS, there's far less boilerplate for the same functionality.
JSON Schema is verbose. A complex request body with nested objects produces a wall of schema definitions that's harder to read and maintain than NestJS DTOs or TypeScript interfaces. The ecosystem is smaller than Express, and significantly smaller than NestJS for organizational patterns. Fastify doesn't provide infrastructure automation, service discovery, or built-in observability.
Consider Fastify when you want schema-driven development, a plugin architecture for organizing a single-service API, and you're comfortable with JSON Schema syntax.
For a deeper comparison, see our Fastify vs Encore.ts article.
Hono comes from the opposite direction than NestJS. It's ultralight, designed for edge runtimes, and has almost no opinions about application architecture. The framework runs on Cloudflare Workers, Deno, Bun, and Node.js, with a ~14kb bundle size.
If your reason for leaving NestJS is that your project is simpler than NestJS assumes, Hono may be a good fit. It's fast, the API is clean, and you can deploy it anywhere. But if you're leaving NestJS because you wanted better infrastructure support or observability, Hono moves you further from those features, not closer.
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const app = new Hono();
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1),
});
app.post('/users', zValidator('json', createUserSchema), async (c) => {
const data = c.req.valid('json');
const user = await createUser(data);
return c.json(user, 201);
});
app.get('/users/:id', async (c) => {
const id = c.req.param('id');
const user = await getUserById(id);
return c.json(user);
});
export default app;
Hono is refreshingly simple after NestJS. No decorators, no DI, no module registration. You define routes, add middleware, and deploy. The cross-runtime support means your code runs in more places than any other framework here.
Hono has no opinions about application architecture beyond middleware. For large projects, you need to establish your own conventions for code organization, database access, testing, and service boundaries. There's no built-in infrastructure support, no observability, and no multi-service tooling.
Consider Hono when you're building lightweight APIs for edge runtimes, when cross-runtime portability matters, or when your project is simple enough that you don't need the structure NestJS provided.
For a detailed comparison, see our Hono vs Encore.ts article.
tRPC takes a fundamentally different approach from NestJS. Instead of building REST endpoints with controllers and decorators, tRPC creates type-safe procedure calls between your frontend and backend. There are no HTTP methods to think about, no URL paths to define, and no separate API client to maintain. The TypeScript types flow from your backend procedures directly to your frontend code.
If you're building a full-stack TypeScript application and the REST layer feels like unnecessary overhead, tRPC eliminates it. Your frontend calls backend functions with full type checking and autocomplete.
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
const appRouter = t.router({
getUser: t.procedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
return await getUserById(input.id);
}),
createUser: t.procedure
.input(z.object({
email: z.string().email(),
name: z.string().min(1),
}))
.mutation(async ({ input }) => {
return await createUser(input);
}),
});
export type AppRouter = typeof appRouter;
tRPC removes an entire category of bugs. When you change a procedure's input or output type, TypeScript catches every caller that needs updating. There's no API client to regenerate, no OpenAPI spec to maintain, and no risk of the frontend and backend drifting out of sync. For full-stack TypeScript apps, this is a productivity multiplier.
tRPC only works when both the frontend and backend are TypeScript. If you need to expose your API to mobile apps, third-party integrations, or non-TypeScript services, tRPC's type-level approach doesn't help. You'll need to add a REST or GraphQL layer alongside it. tRPC also doesn't provide any infrastructure tooling, database management, or observability features. It solves the API layer problem and nothing else.
Consider tRPC when you're building a full-stack TypeScript application, your API is primarily consumed by your own frontend, and end-to-end type safety is your top priority. It pairs well with Next.js and other React meta-frameworks.
For a detailed comparison, see our tRPC vs Encore.ts article.
TypeScript's backend ecosystem has too many valid ways to structure things. AI coding agents pick a different combination of validation library, database access pattern, and project layout on every prompt. NestJS makes this worse because its decorator-heavy approach and deep dependency injection system means agents have to understand modules, providers, guards, and interceptors - too many moving parts for consistent generation.
With Encore, the project's API patterns, infrastructure declarations, and service structure are already defined. An agent reads the existing conventions and follows them. There's one way to define an API, one way to declare a database, and one way to call another service.
Encore also provides an MCP server and editor rules (encore llm-rules init) that give agents access to database schemas, distributed traces, and service architecture. This means agents can generate queries that match your actual tables, debug with real request data, and verify their own work. Read more in How AI Agents Want to Write TypeScript.
If you want structure and infrastructure without boilerplate, Encore.ts is the best alternative to NestJS. It provides multi-service architecture, type-safe service communication, built-in databases, Pub/Sub, cron jobs, and distributed tracing. You get the production-readiness that drew you to NestJS, but with less code to write and maintain. Encore's consistent conventions also mean AI coding agents can follow your project's patterns to generate production-ready code. For most teams building backend systems, Encore is the recommended choice.
If you want maximum flexibility, Express lets you build exactly what you need with no imposed architecture. The tradeoff is assembling everything yourself.
If you want schema-driven validation and docs, Fastify provides JSON Schema validation with OpenAPI generation and a solid plugin system, without the class-based architecture of NestJS.
If you're targeting edge runtimes, Hono is the lightest option with the broadest runtime support. It's the right call for Cloudflare Workers and similar platforms.
If you want end-to-end type safety with your frontend, tRPC eliminates the API boundary between frontend and backend TypeScript code. It's narrowly focused but excellent at what it does.
The pattern most teams follow when leaving NestJS: they wanted structure and production features, but the boilerplate became a tax on every feature. Encore.ts provides that same structure and those same features through a simpler model. Start with the REST API tutorial to see how it feels in practice.
For a broader comparison, see our Best TypeScript Backend Frameworks guide.
Thinking about switching from NestJS? Join our Discord community to discuss your use case with other developers who've made the move.