How to Migrate Express to Fastify for Faster JSON Serialization
Fastify serializes JSON 2-10x faster than Express on data-heavy routes - but only when you declare a response schema. Without it, Fastify falls back to JSON.stringify and the gain nearly disappears. Here's how to write the migration and get the speed.
Why Express JSON serialization is the bottleneck
Express uses JSON.stringify for every response. For a route that returns an array
of 200 customer objects, JSON.stringify reflects over every property on every object
at runtime on every request. At low traffic it's invisible; at a few hundred rps it
shows up clearly in profiling as the hot path.
Fastify solves this with fast-json-stringify: given a JSON Schema for the response
body, it compiles a dedicated serializer at startup that knows exactly which fields to
emit and in what order - no reflection, no per-call key walks.
The Express starting point
import express from "express";
const app = express();
app.get("/api/customers", (req, res) => {
res.json({ customers: CUSTOMERS, count: CUSTOMERS.length });
});
app.listen(8000);
This works, but every call to res.json() invokes JSON.stringify on the full
payload.
The Fastify migration
import Fastify from "fastify";
const fastify = Fastify({ logger: false });
fastify.get(
"/api/customers",
{
schema: {
response: {
200: {
type: "object",
properties: {
count: { type: "integer" },
customers: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "integer" },
name: { type: "string" },
email: { type: "string" },
status: { type: "string" },
signup_date: { type: "string" },
},
},
},
},
},
},
},
},
async () => ({ customers: CUSTOMERS, count: CUSTOMERS.length })
);
fastify.listen({ port: 8000, host: "0.0.0.0" });
The handler itself barely changes. The difference is the schema.response[200] block
passed as the second argument to fastify.get. Fastify reads that schema at startup,
compiles a fast-json-stringify serializer, and uses it for every response - not
JSON.stringify.
Why the schema placement matters
The schema goes inside the route options object - the second positional argument between the URL string and the handler function:
fastify.get(url, { schema: { response: { 200: { ... } } } }, handler)
// ^^^^^^^^ route options ^^^^^^^^
Omitting the schema is the most common migration mistake. Fastify without a response
schema is only marginally faster than Express because it still falls through to
JSON.stringify for serialization.
Key differences from Express
- Handler signature - Fastify handlers are
asyncand return the value directly; nores.json()needed. - Schema is the contract - declare every field you want serialized; undeclared fields are stripped from the response, which also prevents accidental data leaks.
/healthneeds no schema - lightweight endpoints with a single small object are fine without one; the overhead there is negligible.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Declaring a Fastify response schema with JSON Schema properties
- Converting an Express res.json() handler to a Fastify async handler
- Using fast-json-stringify via schema-based route options to eliminate reflection
FAQ
Why is Fastify faster than Express for JSON responses?
Fastify uses fast-json-stringify when you declare a response schema - it compiles a dedicated serializer at startup that skips runtime reflection. Without the schema, Fastify falls back to JSON.stringify and the gain is marginal.
Where does the response schema go in a Fastify route?
In the route options object - the second argument between the URL string and the handler: fastify.get(url, { schema: { response: { 200: { ... } } } }, handler). The schema is keyed by HTTP status code.
Do I need to change the handler when migrating from Express to Fastify?
Mostly no. The main change is making the handler async and returning the value directly instead of calling res.json(). The route logic stays the same; you add the schema options object alongside the handler.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →