Skip to content

Express to Volten Migration Guide

Migrating from Express to Volten is straightforward. Volten is designed to feel familiar to Express developers, while eliminating bloated dependency trees, offering native modern TypeScript support, and delivering significantly higher throughput with an Adaptive JIT Engine.


Why Migrate to Volten?

FeatureExpressVolten
Dependencies30+ npm packages0 dependencies (built strictly on Node.js core)
ArchitectureSeparate (req, res, next)Unified RequestContext (ctx)
Body ParsingRequires body-parser or express.json()Built-in lazy parsing (await ctx.body())
File UploadsRequires multer, busboy, or formidableBuilt-in streaming multipart (ctx.multipart())
CookiesRequires cookie-parserBuilt-in (ctx.cookies, ctx.setCookie)
Static Filesexpress.static() via serve-staticBuilt-in (app.static()) with ETags & traversal protection
Middleware ModelLinear callback chainingModern async onion model (await next())
Error Handling4-parameter middleware (err, req, res, next)Centralized app.onError((err, ctx) => ...)
Edge ReadyNode.js onlyRuns on Node.js and Edge/Fetch environments

Quick Reference: Express vs Volten

Here is a side-by-side syntax comparison for common operations:

ActionExpressVolten
Initialize Appconst app = express();const app = new App();
Listen on Portapp.listen(3000, () => ...);app.listen(3000, () => ...);
Route Parametersreq.params.idctx.params.id (strongly-typed)
Query Stringreq.query.searchctx.query.search
Read JSON Bodyreq.body (requires middleware)await ctx.body()
Read Raw Textreq.body (requires express.text())await ctx.body('text')
Read Streamreq (Node stream)ctx.bodyStream (Web ReadableStream)
Send JSONres.json({ ok: true })ctx.json({ ok: true }) or return { ok: true }
Send Textres.send('hello')ctx.send('hello') or return 'hello'
Set Status Coderes.status(201)ctx.status(201)
Set Headerres.set('X-Name', 'value')ctx.setHeader('X-Name', 'value')
Get Headerreq.get('authorization')ctx.headers['authorization']
Read Cookiesreq.cookies.token (requires package)ctx.cookies.token
Set Cookieres.cookie('token', val, opts)ctx.setCookie('token', val, opts)
Send Static Filesapp.use(express.static('public'))app.static('public')
File Downloadres.download(filePath, 'file.pdf')await ctx.download(filePath, 'file.pdf')

Key Differences Explained

1. Unified Context (ctx) vs req/res

In Express, handlers take (req, res, next). You constantly pass two separate objects around:

javascript
// Express
app.get("/user/:id", (req, res) => {
  const id = req.params.id;
  res.status(200).json({ id });
});

In Volten, everything is unified under a single ctx (RequestContext) object:

typescript
// Volten
app.get("/user/:id", (ctx) => {
  const id = ctx.params.id;
  return ctx.status(200).json({ id });
});

Handler Return Values

In Volten, you can also directly return strings, objects, or Web Standard Response instances. Volten automatically serializes and sends them!

typescript
app.get("/hello", () => ({ message: "Hello World!" }));

2. Request Body Parsing

In Express

You must register middleware globally before routes are declared:

javascript
// Express
const express = require("express");
const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.post("/items", (req, res) => {
  const item = req.body; // already parsed into memory
  res.json({ saved: item });
});

In Volten

Body parsing is lazy and built-in. No middleware setup required:

typescript
// Volten
import { App } from "volten";
const app = new App();

app.post("/items", async (ctx) => {
  const item = await ctx.body(); // parsed on-demand
  return ctx.json({ saved: item });
});

3. File Uploads

In Express

Express requires configuring an external multipart library such as multer:

javascript
// Express
const multer = require("multer");
const upload = multer({ dest: "uploads/" });

app.post("/upload", upload.single("avatar"), (req, res) => {
  console.log(req.file.path);
  res.send("Uploaded");
});

In Volten

Volten includes streaming multipart uploads out-of-the-box:

typescript
// Volten
app.post("/upload", async (ctx) => {
  for await (const part of ctx.multipart()) {
    if (part.isFile && part.name === "avatar") {
      await part.save(`./uploads/${part.filename}`);
    }
  }
  return ctx.send("Uploaded");
});

4. Middleware: The Async Onion Model

Express uses a callback-based middleware chain where forgetting to call next() or calling it after ending the response can lead to subtle hanging requests or crashes:

javascript
// Express
app.use((req, res, next) => {
  const start = Date.now();
  res.on("finish", () => {
    console.log(`Duration: ${Date.now() - start}ms`);
  });
  next();
});

Volten uses an async onion model (similar to Koa). You await next() to execute downstream handlers, and post-processing occurs cleanly when execution unrolls:

typescript
// Volten
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  console.log(`${ctx.method} ${ctx.path} - ${Date.now() - start}ms`);
});

Next Call Safety

Volten actively detects duplicate next() invocations or calling next() after a response has been sent, safely throwing InvalidNextCallError to prevent silent corruption.


5. Error Handling

In Express

Express identifies error handlers by checking function arity fn.length === 4:

javascript
// Express
app.use((err, req, res, next) => {
  console.error(err);
  res.status(err.status || 500).json({ error: err.message });
});

If you accidentally define (err, req, res) with 3 arguments, Express silently treats it as regular middleware, leaving errors unhandled.

In Volten

Error handling is explicit and registered with app.onError():

typescript
// Volten
app.onError((err, ctx) => {
  console.error(`[Error] ${ctx.method} ${ctx.path}:`, err.message);
  return ctx.status(err.statusCode || 500).json({
    success: false,
    error: err.message,
  });
});

Interactive Migration Morph

Watch how cleanly your Express code transforms into Volten. Switch between Express (Before) and Volten (After), or press Auto-Morph to animate the token transitions powered by @shikijs/magic-move:


Comprehensive Migration Example

Below is a complete side-by-side comparison showing how an Express service with authentication, body parsing, static files, and error handling translates to Volten.

Before: Express Application

javascript
const express = require("express");
const cookieParser = require("cookie-parser");
const path = require("path");

const app = express();

// Middleware
app.use(express.json());
app.use(cookieParser());
app.use("/static", express.static(path.join(__dirname, "public")));

// Auth middleware
const requireAuth = (req, res, next) => {
  const token = req.cookies.authToken;
  if (!token) {
    return res.status(401).json({ error: "Unauthorized" });
  }
  req.userId = "user_123";
  next();
};

// Routes
app.get("/api/profile", requireAuth, (req, res) => {
  res.json({ userId: req.userId, name: "Alice" });
});

app.post("/api/items", requireAuth, (req, res) => {
  const data = req.body;
  res.status(201).json({ created: true, data });
});

// 404 Handler
app.use((req, res) => {
  res.status(404).json({ error: "Not Found" });
});

// Error Handler
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: "Internal Server Error" });
});

app.listen(3000, () => {
  console.log("Express running on port 3000");
});

After: Volten Application

typescript
import { App } from "volten";

const app = new App();

// Serve static files
app.static("public");

// Auth middleware using ctx.state and ctx.cookies
const requireAuth = async (ctx, next) => {
  const token = ctx.cookies.authToken;
  if (!token) {
    return ctx.status(401).json({ error: "Unauthorized" });
  }
  ctx.state.userId = "user_123";
  await next();
};

// Routes
app.get("/api/profile", requireAuth, (ctx) => {
  return ctx.json({ userId: ctx.state.userId, name: "Alice" });
});

app.post("/api/items", requireAuth, async (ctx) => {
  const data = await ctx.body();
  return ctx.status(201).json({ created: true, data });
});

// Global Error Handler (automatically handles 404s and thrown errors)
app.onError((err, ctx) => {
  const status = err.statusCode || 500;
  return ctx.status(status).json({ error: err.message });
});

app.listen(3000, () => {
  console.log("Volten running on port 3000");
});

Migration Checklist

  1. [ ] Remove unused packages: Uninstall body-parser, cookie-parser, serve-static, and multer.
  2. [ ] Change handler signature: Update (req, res) to (ctx).
  3. [ ] Convert parameters: Change req.params.x to ctx.params.x.
  4. [ ] Convert body access: Replace req.body with await ctx.body().
  5. [ ] Update middleware: Replace (req, res, next) with async (ctx, next) => { await next(); }.
  6. [ ] Use ctx.state: Migrate arbitrary req properties (like req.user) to ctx.state.user.
  7. [ ] Register error handler: Replace 4-argument middleware with app.onError((err, ctx) => ...).

Released under the MIT License.