NextRush
Guides

Coming from Express

Migrate your Express application to NextRush — concept mapping, pattern translation, and step-by-step migration path.

This guide maps Express patterns to their NextRush equivalents. Most of your existing knowledge transfers — the mental model changes are small.

Concept Mapping

ExpressNextRushNotes
req + res + nextSingle ctx objectEverything lives on context
app.get(path, handler)router.get(path, handler)Routes registered on routers
app.use(middleware)app.use(middleware)Same API, different signature
req.paramsctx.paramsSame functionality
req.queryctx.querySame functionality
req.bodyctx.bodyRequires @nextrush/body-parser
res.json(data)ctx.json(data)Same behavior
res.status(code)ctx.status = codeProperty instead of method
res.set(name, value)ctx.set(name, value)Same API
res.redirect(url)ctx.redirect(url)Same API
next() (callback)await ctx.next() (async)Onion model instead of linear

Step 1 — Project Setup

package.json
{
  "dependencies": {
    "express": "^4.21.0",
    "cors": "^2.8.5",
    "helmet": "^8.0.0",
    "body-parser": "^1.20.2"
  },
  "devDependencies": {
    "@types/express": "^5.0.0",
    "@types/cors": "^2.8.17"
  }
}
package.json
{
  "dependencies": {
    "nextrush": "^3.0.0"
  }
}

All middleware (cors, helmet, body-parser) are built into the nextrush meta-package. Zero @types packages needed — TypeScript types are included.


Step 2 — App Initialization

import express from 'express';
import cors from 'cors';
import helmet from 'helmet';

const app = express();

app.use(helmet());
app.use(cors());
app.use(express.json());
import { createApp, createRouter, listen } from 'nextrush';
import { cors } from '@nextrush/cors';
import { helmet } from '@nextrush/helmet';
import { bodyParser } from '@nextrush/body-parser';

const app = createApp();

app.use(helmet());
app.use(cors());
app.use(bodyParser());

What changed: Factory functions (createApp) instead of constructors. Middleware packages are explicit imports.


Step 3 — Routes

app.get('/users', (req, res) => {
  const page = parseInt(req.query.page) || 1;
  res.json({ users: getUsers(page), page });
});

app.get('/users/:id', (req, res) => {
  const user = findUser(req.params.id);
  if (!user) {
    return res.status(404).json({ error: 'Not found' });
  }
  res.json(user);
});

app.post('/users', (req, res) => {
  const user = createUser(req.body);
  res.status(201).json(user);
});
const users = createRouter();

users.get('/', (ctx) => {
  const page = parseInt(ctx.query.page ?? '1');
  ctx.json({ users: getUsers(page), page });
});

users.get('/:id', (ctx) => {
  const user = findUser(ctx.params.id);
  if (!user) throw new NotFoundError('Not found');
  ctx.json(user);
});

users.post('/', (ctx) => {
  const user = createUser(ctx.body);
  ctx.status = 201;
  ctx.json(user);
});

app.route('/users', users);

What changed:

  • Routes live on a Router instance, mounted with app.route(prefix, router)
  • req + res merged into single ctx
  • Errors thrown, not manually formatted — NotFoundError auto-sends 404

Step 4 — Middleware

The biggest conceptual change. Express middleware is linear — NextRush uses the onion model.

Loading diagram...
// Request timing (requires 'finish' event)
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    console.log(`${req.method} ${req.url} ${Date.now() - start}ms`);
  });
  next();
});

// Auth check
app.use((req, res, next) => {
  const token = req.headers.authorization;
  if (!token) return res.status(401).json({ error: 'Unauthorized' });
  req.user = verifyToken(token);
  next();
});
// Request timing (before AND after in one function)
app.use(async (ctx) => {
  const start = Date.now();
  await ctx.next();
  console.log(`${ctx.method} ${ctx.path} ${Date.now() - start}ms`);
});

// Auth check
app.use(async (ctx) => {
  const token = ctx.get('authorization');
  if (!token) throw new UnauthorizedError('Unauthorized');
  ctx.state.user = verifyToken(token);
  await ctx.next();
});

Key benefit: The onion model lets you write "before" and "after" logic in one function. No res.on('finish') hacks needed.


Step 5 — Error Handling

// Must be last middleware, must have exactly 4 params
app.use((err, req, res, next) => {
  console.error(err);
  res.status(err.status || 500).json({
    error: err.message,
  });
});
import { NotFoundError, BadRequestError } from '@nextrush/errors';

// Throw typed errors — automatic status codes
users.get('/:id', (ctx) => {
  const user = findUser(ctx.params.id);
  if (!user) throw new NotFoundError('User not found');
  ctx.json(user);
});

// Optional: custom error handler
app.setErrorHandler((error, ctx) => {
  console.error(error);
  ctx.status = error.status ?? 500;
  ctx.json({ error: error.message });
});

What changed: Typed error classes replace manual status code management. Throw errors instead of calling next(err).


Step 6 — Start the Server

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
await listen(app, 3000);
console.log('Server running on port 3000');

Common Middleware Translation

Express PackageNextRush EquivalentInstall
cors@nextrush/corsIncluded in nextrush
helmet@nextrush/helmetIncluded in nextrush
body-parser / express.json()@nextrush/body-parserIncluded in nextrush
express-rate-limit@nextrush/rate-limitIncluded in nextrush
cookie-parser@nextrush/cookiesIncluded in nextrush
compression@nextrush/compressionIncluded in nextrush
serve-static@nextrush/static@nextrush/static
express-sessionNo built-inUse custom middleware
passportGuards systemSee Guards

From Fastify

If you're coming from Fastify, the key differences are:

FastifyNextRushNotes
request + replySingle ctxUnified context object
Lifecycle hooks (onRequest, onSend)Middleware onion modelBefore/after in one function
JSON Schema validationTransform decoratorsUse Zod, Valibot, or any library
fastify-plugin encapsulationPlugin interfaceSimpler, no encapsulation scope
fast-json-stringify (AOT)Native JSON.stringify~13% throughput difference
Node.js onlyNode, Bun, Deno, EdgeMulti-runtime support
// Fastify
app.addHook('onRequest', async (request, reply) => {
  /* ... */
});
app.get('/users', async (request, reply) => {
  return users;
});

// NextRush equivalent
app.use(async (ctx) => {
  /* ... */ await ctx.next();
});
router.get('/users', (ctx) => ctx.json(users));

Checklist

Before migrating, verify:

  • All routes use ctx instead of req/res
  • Middleware calls await ctx.next() (not callback next())
  • Errors throw typed classes (NotFoundError, BadRequestError)
  • Body parsing uses @nextrush/body-parser middleware
  • Tests use app.callback() for integration testing
  • Custom error handler registered via app.setErrorHandler()

Need help?

Open an issue on GitHub if you hit a migration edge case not covered here.

On this page