Framework Comparison
Side-by-side code comparison of NextRush, Express, Fastify, Hono, and Koa — routing, middleware, error handling, and TypeScript support.
How NextRush compares to popular Node.js frameworks — not only speed, but how you write code.
Looking for benchmarks?
This page compares code patterns and architecture. For raw performance numbers, see Performance.
At a Glance
| Feature | NextRush | Express | Fastify | Hono | Koa |
|---|---|---|---|---|---|
| TypeScript | First-class (strict) | Bolt-on (@types) | Built-in | Built-in | Bolt-on (@types) |
| Runtime deps | 0 | 31 | 15 | 0 | 24 |
| Multi-runtime | Node, Bun, Deno, Edge | Node only | Node only | Node, Bun, Deno, Edge | Node only |
| Context API | Single ctx object | req + res + next | request + reply | Single c object | Single ctx object |
| Middleware model | Koa-style onion | Linear chain | Hooks + lifecycle | Koa-style onion | Onion model |
| Built-in DI | Yes (@nextrush/di) | No | No | No | No |
| Decorators | Yes (@Controller, @Get) | No | Via plugins | No | No |
| Plugin system | Plugin interface | app.use() | Encapsulated plugins | Middleware only | app.use() |
| Routing | Radix tree (O(k)) | Linear scan (O(n)) | Radix tree (O(k)) | Radix tree (O(k)) | Via koa-router |
Hello World
The same endpoint in every framework.
import { createApp, createRouter, listen } from 'nextrush';
const app = createApp();
const router = createRouter();
router.get('/', (ctx) => ctx.json({ message: 'Hello World' }));
app.route('/', router);
await listen(app, 3000);import express from 'express';
const app = express();
app.get('/', (req, res) => {
res.json({ message: 'Hello World' });
});
app.listen(3000);import Fastify from 'fastify';
const app = Fastify();
app.get('/', async () => {
return { message: 'Hello World' };
});
await app.listen({ port: 3000 });import { Hono } from 'hono';
import { serve } from '@hono/node-server';
const app = new Hono();
app.get('/', (c) => c.json({ message: 'Hello World' }));
serve({ fetch: app.fetch, port: 3000 });import Koa from 'koa';
import Router from 'koa-router';
const app = new Koa();
const router = new Router();
router.get('/', (ctx) => {
ctx.body = { message: 'Hello World' };
});
app.use(router.routes());
app.listen(3000);Middleware
How each framework composes middleware — logging, auth, and error handling.
Koa-style onion model. Code before ctx.next() runs on the way in, code after runs on the way out.
// Timing middleware
app.use(async (ctx) => {
const start = Date.now();
await ctx.next();
ctx.set('X-Response-Time', `${Date.now() - start}ms`);
});
// Auth middleware
app.use(async (ctx) => {
const token = ctx.get('authorization');
if (!token) throw new UnauthorizedError('Missing token');
ctx.state.user = verifyToken(token);
await ctx.next();
});Linear chain with next() callback. Error handling is a separate signature.
// Timing middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
res.set('X-Response-Time', `${Date.now() - start}ms`);
});
next();
});
// Auth middleware
app.use((req, res, next) => {
const token = req.headers.authorization;
if (!token) return res.status(401).json({ error: 'Missing token' });
req.user = verifyToken(token);
next();
});
// Error middleware (must have 4 args)
app.use((err, req, res, next) => {
res.status(err.status || 500).json({ error: err.message });
});Hook-based lifecycle instead of middleware chain.
// Timing via hooks
app.addHook('onRequest', async (request, reply) => {
request.startTime = Date.now();
});
app.addHook('onSend', async (request, reply) => {
reply.header('X-Response-Time', `${Date.now() - request.startTime}ms`);
});
// Auth via hook
app.addHook('onRequest', async (request, reply) => {
const token = request.headers.authorization;
if (!token) {
reply.code(401).send({ error: 'Missing token' });
return;
}
request.user = verifyToken(token);
});Onion model — same pattern as NextRush.
// Timing middleware
app.use(async (ctx, next) => {
const start = Date.now();
await next();
ctx.set('X-Response-Time', `${Date.now() - start}ms`);
});
// Auth middleware
app.use(async (ctx, next) => {
const token = ctx.get('authorization');
if (!token) ctx.throw(401, 'Missing token');
ctx.state.user = verifyToken(token);
await next();
});Key difference: NextRush and Koa share the onion model — you write both "before" and "after" logic in one function. Express uses linear chains with separate error middleware. Fastify uses lifecycle hooks.
Error Handling
Typed error classes with automatic status codes. Thrown errors become JSON responses.
import { NotFoundError, ValidationError } from '@nextrush/errors';
router.get('/users/:id', (ctx) => {
const user = db.find(ctx.params.id);
if (!user) throw new NotFoundError('User not found');
ctx.json(user);
});
// Custom error handler
app.setErrorHandler((error, ctx) => {
ctx.status = error.status ?? 500;
ctx.json({
error: error.message,
code: error.code,
});
});Manual status codes. Error middleware requires 4 parameters.
app.get('/users/:id', (req, res, next) => {
const user = db.find(req.params.id);
if (!user) {
const err = new Error('User not found');
err.status = 404;
return next(err);
}
res.json(user);
});
// Error middleware (must be last, must have 4 args)
app.use((err, req, res, next) => {
res.status(err.status || 500).json({ error: err.message });
});Schema-based error serialization. Custom error handler per route or global.
app.get('/users/:id', async (request, reply) => {
const user = db.find(request.params.id);
if (!user) {
reply.code(404);
return { error: 'User not found' };
}
return user;
});
app.setErrorHandler(async (error, request, reply) => {
reply.status(error.statusCode || 500).send({
error: error.message,
});
});TypeScript Support
| Aspect | NextRush | Express | Fastify |
|---|---|---|---|
| Source language | TypeScript | JavaScript | TypeScript |
| Types included | Yes (strict mode) | No — install @types/express | Yes |
| Route param inference | Via generics on ctx | Manual casting required | Via JSON Schema |
| Body type safety | Transform decorators (Zod, etc.) | None built-in | JSON Schema validation |
| Strict mode | Enforced — zero any | Optional | Optional |
// NextRush — types flow naturally
router.get('/users/:id', (ctx) => {
const id = ctx.params.id; // string (typed)
const page = ctx.query.page; // string | undefined (typed)
ctx.json({ id, page }); // return type checked
});When to Choose What
| Your priority | Likely fit | Notes |
|---|---|---|
| Raw throughput (Node.js only) | Fastify | AOT JSON serialization, mature radix tree |
| Multi-runtime (Bun, Deno, Edge) | NextRush or Hono | Compare adapters and deploy targets |
| Strict TypeScript + small core deps | NextRush | Core packages avoid extra runtime dependencies |
| Large ecosystem + community | Express | Broad middleware ecosystem and tutorials |
| Class-based + DI | NextRush | Optional decorators and DI in the same stack |
| Minimal learning curve | Koa or Express | Familiar patterns and documentation |
| Mixed functional + class routes | NextRush | Both styles can coexist in one app |
Migration Paths
Ready to switch? Follow the step-by-step guides: