What Fastify Is & Your First Server
You know JavaScript — functions, async/await, modules — and you've
probably met Express, the minimalist Node web framework everyone starts
with. Fastify is the one you reach for when Express's "everything's bolted on, figure it out yourself"
approach starts to bite. It's an Express alternative built around two
promises: it's genuinely fast (one of the quickest Node frameworks), and it's schema-first —
input validation and JSON serialization are baked in, not something you wire up later.
Like Express, Fastify doesn't replace Node — it sits on top of the same built-in HTTP server you'd otherwise drive raw (the node:http guide shows exactly what's under there, and it's worth seeing once). If you've read What a Framework Even Is, Fastify is the textbook case of an opinionated one: it has firm ideas about how you should structure things, and those opinions are what give you speed and safety for free.
The mental model: two pillars
Before any code, hold the two ideas that explain everything Fastify does. The rest of this guide is just these two getting deeper.
💡 A route is a handler plus a schema. A handler is the function that runs when a request arrives. A schema is a description (in JSON Schema) of what the request body, params, and response should look like. Hand Fastify both and it validates incoming requests and serializes responses fast, for free. You'll write handlers today; schemas arrive in Phase 2.
💡 The app is a tree of plugins. Everything you add — routes, shared logic, decorators — gets registered as a plugin, and plugins nest inside other plugins. That tree is how Fastify keeps a large app organized instead of one giant file. We build the trunk today; the branches come in Phase 3.
Hold those two — handler + schema, tree of plugins — and Fastify stops looking like magic.
Your first server
One install gets you the framework:
What just happened: npm downloaded Fastify and its dependencies into node_modules and recorded
it in your package.json. The whole framework is now available to require (or import). This
assumes you've run npm init -y first to create a package.json — if you haven't, do that, then
install.
Now the smallest Fastify server that does something real. Create a file called index.js:
const Fastify = ;
const app = ;
app.;
app.;
What just happened: four moves, and they're the four you'll use forever.
const app = Fastify({ logger: true });calls the Fastify function to create your instance — the object that holds your routes and runs the show. The{ logger: true }option turns on Fastify's built-in logger (the fast pino logger), so every request gets logged with no extra setup.app.get('/', async (request, reply) => { ... })registers a route: "when aGETarrives for/, run this handler." You never call the handler yourself — Fastify calls it when a matching request comes in. Notice it's anasyncfunction taking(request, reply): the request (what the client sent) and the reply (how you respond).- The handler returns a value, and Fastify sends it as the response. Return a plain object and
Fastify serializes it to JSON and sets
Content-Type: application/jsonfor you — nores.json()ceremony. Whatever youreturnis the response body. app.listen({ port: 3000 }, callback)starts the underlying HTTP server on port 3000. The callback fires once it's up (or with anerrif the port's taken);addressis the URL it bound to.
⚠️ Fastify's listen takes an options object — { port: 3000 }, not bare 3000. This trips up
people coming from Express, where app.listen(3000) works. In Fastify 4 and 5 the first argument is
an options object ({ port, host }); passing a bare number is deprecated and will bite you. Get this
into your fingers early.
Run it with plain Node — Fastify is a library, there's no special CLI:
$ node index.js
{"level":30,"time":1718000000000,"pid":12345,"hostname":"laptop","msg":"Server listening at http://127.0.0.1:3000"}
{"level":30,"time":1718000000001,"pid":12345,"hostname":"laptop","msg":"listening on http://127.0.0.1:3000"}
What just happened: Node executed your file, Fastify handed its request handler to node:http, and
the server is listening. Those JSON lines are the built-in logger talking — the first is Fastify's own
startup message, the second is your app.log.info call. The server keeps running, waiting for
requests, until you stop it with Ctrl+C. Open a second terminal and hit it:
$ curl localhost:3000
{"hello":"world"}
What just happened: curl sent a GET /. Fastify matched it to your route, called the handler, took
its returned object, serialized it to JSON, and sent it back — and logged the request in your first
terminal as it did. A working JSON API in about seven lines.
Returning a value vs. reply.send()
Returning a value is the idiomatic Fastify way, and it's enough most of the time. But sometimes you
need to control the status code or set headers — that's what the reply object is for. Compare:
// Style 1: return the value — Fastify sends it as JSON with status 200
app.;
// Style 2: use reply when you need a non-200 status or headers
app.;
What just happened: both routes respond with JSON. The first returns an object — the clean
default, status 200. The second calls reply.code(201).send(obj) to set a 201 Created status and
send the body. Rule of thumb: return when 200 is fine; reach for reply when you need a different
status or custom headers. (One catch: if you call reply.send(), don't also return a value from
the same handler — pick one. Sending twice is an error, the same way it is in Express.)
The built-in logger you turned on with { logger: true } is always available as app.log (and
per-request as request.log). It writes structured JSON — ugly to read raw, but exactly what log
tooling wants in production. For pretty local output you can pipe it through pino-pretty, but that's
a Phase 7 concern; for now, know that real logging is on by default.
The running example: a books API
One more thing before we go deeper. Across this guide we grow one real service so each concept lands on something concrete instead of a toy. Meet the books API — a small catalog backend where each book is an object shaped like this:
const books = ;
app.;
What just happened: books is an in-memory array of objects, each with id, title, and author.
The GET /books route returns the whole list; Fastify serializes the array to JSON. This is the first
endpoint of an API we'll turn into full create/read/update/delete (CRUD) — with schemas validating the
input and plugins organizing the code — over the coming phases. In-memory means the data resets every
restart; that's fine for learning, and we'll talk real storage later. Hitting it:
$ curl localhost:3000/books
[{"id":1,"title":"The Pragmatic Programmer","author":"Hunt & Thomas"},{"id":2,"title":"Clean Code","author":"Robert Martin"}]
What just happened: the route returned the array, Fastify serialized it, and curl printed the JSON.
You've now seen the full shape of a Fastify endpoint — method, path, async handler, returned value —
and you have a real API with one route. Next we make routes carry data (a book's id in the URL,
query strings) and attach schemas that validate it, which is
Phase 2: Routing & Schemas.
Recap
- Fastify is the fast, schema-first Node web framework — an Express
alternative that bakes in JSON-schema validation and fast serialization. It sits on top of
node:http, like Express does. Install withnpm install fastify. - The two big ideas: a route is a handler plus a schema, and the app is a tree of plugins. Schemas land in Phase 2, plugins in Phase 3 — today you write handlers and start the tree.
- A first server is four moves:
Fastify({ logger: true })creates the instance (with a built-in logger);app.get(path, async (request, reply) => …)registers a route; the handler's returned value is sent as JSON;app.listen({ port })starts it. Run with plainnode index.js. - ⚠️
app.listentakes an options object ({ port: 3000 }), not a bare number — a common gotcha coming from Express. - Return a value for the clean 200 case; reach for
reply.code(n).send(obj)when you need a specific status (like201) or custom headers. Don't return and send from the same handler. - Our running example is a books API (
{ id, title, author }), starting from a singleGET /booksroute and growing into full CRUD with schemas and plugins across the guide.
Quick check
Three questions on what has to stick — what Fastify is, how a first server is wired, and how handlers respond:
[
{
"q": "What is Fastify, in one line?",
"choices": [
"A fast, schema-first Node.js web framework — an Express alternative with validation and JSON serialization built in",
"A database for storing JSON documents in Node apps",
"A standalone web server written in Rust that replaces Node entirely",
"A frontend UI library for building components in the browser"
],
"answer": 0,
"explain": "Fastify is a fast, schema-first Node web framework. It's an Express alternative that bakes in JSON-schema validation and fast serialization, and like Express it runs on top of node:http rather than replacing Node."
},
{
"q": "In a Fastify handler `async (request, reply) => { return { hello: 'world' }; }`, what happens to the returned object?",
"choices": [
"Fastify serializes it to JSON, sets Content-Type: application/json, and sends it as the response with status 200",
"Nothing — you must call reply.json() yourself to send a response",
"It's stored in memory and sent only on the next request",
"It throws an error because handlers must not return a value"
],
"answer": 0,
"explain": "In Fastify, the value a handler returns becomes the response: a plain object is serialized to JSON with the JSON content type and sent with status 200. You only reach for reply (e.g. reply.code(201).send(obj)) when you need a different status or custom headers."
},
{
"q": "Which call correctly starts a Fastify server on port 3000?",
"choices": [
"app.listen({ port: 3000 }, (err, address) => { ... })",
"app.listen(3000, () => { ... })",
"app.start(3000)",
"app.serve({ on: 3000 })"
],
"answer": 0,
"explain": "Fastify's listen takes an options object — { port: 3000 } — not a bare number. Passing app.listen(3000) is the Express habit that's deprecated in Fastify and will bite you; always pass the options object."
}
]
Guide overview · Phase 2: Routing & Schemas →
Check your understanding
1. What is Fastify, in one line?
2. In a Fastify handler `async (request, reply) => { return { hello: 'world' }; }`, what happens to the returned object?
3. Which call correctly starts a Fastify server on port 3000?