Node.js is a popular choice for teams building web apps and SaaS services. When choosing a backend framework for Node.js, these days you have a big range of options, each framework with its unique strengths and ideal use cases.
Picking the right framework can be difficult. So in this article we take a look at the leading frameworks and figure out when to choose which framework.
Here's a high-level comparison of the frameworks included in this article, comparing built-in support for common backend requirements.
| Feature | Express.js | Encore | Fastify | NestJS | Hono |
|---|---|---|---|---|---|
| Use case | General purpose APIs | Distributed systems | Schema-validated APIs | Enterprise applications | Edge & serverless |
| Learning Curve | Low | Low | Medium | High | Low |
| Built-in Validation | No | Yes | Yes (JSON Schema) | Yes (decorators) | Yes |
| Type-safe Service Calls | No | Yes | No | No | No |
| Built-in Tracing | No | Yes | No | No | No |
| Infrastructure from Code | No | Yes | No | No | No |
| Auto API Documentation | No | Yes | Yes (via plugins) | Yes (via plugins) | No |
| Service Discovery | No | Yes | No | No | No |
Express.js, is in essence the most common Node.js web framework. It offers a minimalistic and flexible approach, and has become for its speed and minimal overhead, providing just the core framework features needed to build web applications and APIs.
Encore is a modern alternative, aimed at teams looking for an integrated and developer-experience focused approach to backend development. Encore focuses on making it simpler to build event-driven and distributed systems, and solves for both the local development experience and automates cloud deployment to your cloud on AWS and GCP.
It works by providing an Open Source Backend Framework that lets you declare services, APIs, and infrastructure semantics as part of the application code. Encore then parses the code and creates a fully type-safe model of your application's microservices, APIs, and infrastructure requirements. Encore uses this model to automatically run your local environment, deploy to temporary preview environments for testing, and provision infrastructure in your cloud (AWS/GCP).
This means you avoid needing to spend time on infrastructure configuration like Terraform, and you don't need to configure and manage Docker Compose manifests for local development.
Encore provides a high-performance distributed systems runtime in Rust, which integrates with the standard Node.js runtime for executing JavaScript code. This ensures 100% compatibility with the Node.js ecosystem.
The Rust runtime does everything from handling incoming requests and making API calls, to querying databases and using Pub/Sub. It even handles all application observability, like distributed tracing, structured logging, and metrics.
This approach leads to a massive performance boost over other frameworks (learn more):
And what’s really cool: Encore has zero NPM dependencies, improving security and speeding up builds and application startup times.
This means that the Node.js event loop — which is single-threaded — can focus on executing your business logic. Everything else happens in Encore’s multi-threaded Rust runtime.
Normally with TypeScript, the type information is lost at runtime. But Encore is different.
Encore uses static code analysis to parse and analyze the TypeScript types you define, and uses the API schema to automatically validate incoming requests, guaranteeing complete type-safety, even at runtime. This means no more confusing exceptions because a required field is missing.
With Encore you define a service by creating a folder and inside that folder defining an API within a regular TypeScript file. Encore recognizes this as a service, and uses the folder name as the service name. When deploying, Encore will automatically provision the required infrastructure for each service.
On disk it might look like this:
/my-app
├── encore.app // ... and other top-level project files
├── package.json
│
├── hello // hello service (a folder)
│ ├── hello.ts // hello service code
│ └── hello_test.ts // tests for hello service
│
└── world // world service (a folder)
└── world.ts // world service code
This means building a microservices architecture is as simple as creating multiple directories within your application.
Encore lets you easily define type-safe, idiomatic TypeScript API endpoints.
It's simple to accept both the URL path parameters, as well as JSON request body data, HTTP headers, and query strings.
It's done in a way that is fully declarative, enabling Encore to automatically parse and validate the incoming request and ensure it matches the schema, with zero boilerplate.
To define an API, use the api function from the encore.dev/api module to wrap a regular TypeScript async function that receives the request data as input and returns response data.
This tells Encore that the function is an API endpoint. Encore will then automatically generate the necessary boilerplate at compile-time.
In the example below, we define the API endpoint ping which accepts POST requests and is exposed as hello.ping (because our service name is hello).
// inside the hello.ts file
import { api } from "encore.dev/api"
export const ping = api(
{ method: "POST" },
async (p: PingParams): Promise<PingResponse> => {
return { message: `Hello ${p.name}!` };
}
);
Publishers & Subscribers (Pub/Sub) let you build systems that communicate by broadcasting events asynchronously. This is a great way to decouple services for better reliability and responsiveness.
Encore's Backend Framework lets you use Pub/Sub in a cloud-agnostic declarative fashion. At deployment, Encore automatically provisions the required infrastructure.
The core of Pub/Sub is the Topic, a named channel on which you publish events.
Encore makes it very simple to create Topics. For example, here's how you create a topic with events about user signups:
import { Topic } "encore.dev/pubsub"
export interface SignupEvent {
userID: string;
}
export const signups = new Topic<SignupEvent>("signups", {
deliveryGuarantee: "at-least-once",
});
To publish an Event, call publish on the topic passing in the event object (which is the type specified in the new Topic<Type> constructor):
const messageID = await signups.publish({userID: id});
// If we get here the event has been successfully published,
// and all registered subscribers will receive the event.
// The messageID variable contains the unique id of the message,
// which is also provided to the subscribers when processing the event.
$ brew install encoredev/tap/encore
Nest.js brings a structured, Angular-inspired framework to the backend, embracing TypeScript as a first-class citizen. It offers a modular architecture that organizes code into separate modules, designed to make it easier to maintain and develop larger applications.
Fastify positions itself as a high-performance alternative to Express, though recent benchmarks show this advantage has diminished. Modern alternatives like Encore now significantly outperform both Fastify and Express in request throughput.
That said, Fastify's value proposition goes beyond raw speed. It uses JSON Schema for validation, which provides both runtime checking and automatic serialization optimization. The plugin architecture keeps code organized and encapsulated, with each plugin having its own dependencies and lifecycle. Fastify also offers good TypeScript support and helpful error messages.
import Fastify from 'fastify';
const fastify = Fastify({ logger: true });
fastify.get('/users/:id', {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' }
}
},
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' }
}
}
}
}
}, async (request, reply) => {
return { id: request.params.id, name: 'John' };
});
fastify.listen({ port: 3000 });
Fastify provides a good balance of performance and features. The JSON Schema validation provides runtime safety and can be used to generate OpenAPI documentation. The plugin system encourages good architectural patterns and makes testing easier.
JSON Schema definitions can become verbose, especially for complex objects. The ecosystem is smaller than Express, so you may need to write custom plugins for less common use cases. The mental model for plugins takes some time to internalize.
Consider Fastify when you appreciate a well-structured plugin architecture, when you want JSON Schema validation with OpenAPI generation, or when you're building a single-service API and are already familiar with the Fastify ecosystem.
Hono is a lightweight framework designed for edge runtimes like Cloudflare Workers, Deno, and Bun. It offers a minimal approach aimed at edge performance.
Strengths:
Considerations:
const app = new Hono();
app.get('/users/:id', async (c) => {
const id = c.req.param('id');
return c.json(await getUserById(id));
});
Verdict: Hono is excellent for edge functions but not designed for full backend development with databases, queues, and microservices.
Node developers are spoiled with many great frameworks to choose from. However, Encore stands out as the only framework that comes with a solution for automated deployment to production-ready infrastructure on AWS/GCP. This makes it a great choice for startups building with speed and scalability in mind.
Want to jump straight to a running app? Clone this starter and deploy it to your own cloud.
It depends on whether you also need your infrastructure managed. If you do, Encore is the strongest choice: you declare the infrastructure your backend needs as typed objects in your code, such as a Postgres database or object storage, and Encore provisions it automatically, with no separate Terraform, YAML, or cloud console step to keep in sync. If you only need HTTP routing and will manage infrastructure separately, Express is the established minimal option, NestJS fits large teams wanting enforced structure, Fastify suits schema-validated services, and Hono targets edge runtimes.
Express.js is the most widely used Node.js framework. It is a minimal, unopinionated web framework that provides core routing and middleware, and its large ecosystem makes it a common default for building APIs and web apps. Newer frameworks like Fastify, NestJS, and Hono have grown in popularity for specific use cases such as performance, structure, or edge runtimes.
Frameworks with strong conventions work best with AI coding agents, because the agent has fewer open-ended decisions to get wrong. NestJS gives an agent a clear module and dependency-injection structure to follow, and Fastify's schema-first routes are predictable to generate. Encore goes further: agents tend to write application logic well but fumble infrastructure like Terraform and IAM, and because Encore expresses infrastructure as typed code, an agent can generate a backend and its infrastructure together with the type system limiting what it misconfigures. Express is hardest to target because it imposes no structure.
Fastify was designed to be faster than Express and has historically shown higher request throughput, in part due to its JSON Schema based serialization. Recent benchmarks show the gap between the two has narrowed. For most applications, framework overhead is small relative to database and network latency, so architecture and I/O usually matter more than raw framework speed.
It depends on the framework. Express, Fastify, NestJS, and Hono handle only your application code, so you set up hosting, databases, and other infrastructure separately using tools like Terraform, Docker, or your cloud provider's console. Encore takes a different approach: it provisions the infrastructure your backend declares into your own AWS or GCP account as standard cloud resources, and 'encore build docker' exports a standard image you can run anywhere, so lock-in stays low and you keep control of where it runs.
NestJS, Fastify, Hono, and Encore all offer first-class TypeScript support out of the box. NestJS is built around TypeScript and decorators, while Encore uses static analysis of your types to validate requests at runtime. Express can be used with TypeScript through community type definitions but is written in plain JavaScript and is less type-aware by default.