Why Fastify for production APIs
Fastify earns its name — it's one of the fastest Node.js web frameworks, with first-class JSON schema validation and a plugin architecture that scales to large apps without becoming spaghetti. It also has excellent built-in logging via Pino. Let's deploy a production-grade Fastify service.
Step 1: A Fastify app with schema validation
Schema validation isn't optional in production — it's your first line of defense and a free performance boost (Fastify compiles schemas to fast serializers).
// app.js
import Fastify from 'fastify';
export function build() {
const app = Fastify({ logger: true });
app.get('/health', async () => ({ status: 'ok' }));
app.post('/users', {
schema: {
body: {
type: 'object',
required: ['name', 'email'],
properties: { name: { type: 'string' }, email: { type: 'string', format: 'email' } },
},
},
}, async (req) => createUser(req.body));
return app;
}Step 2: Bind to $PORT and listen on all interfaces
// server.js
import { build } from './app.js';
const app = build();
const port = Number(process.env.PORT) || 3000;
await app.listen({ port, host: '0.0.0.0' });The host: '0.0.0.0' is essential inside containers — the default localhost won't accept external traffic.
Step 3: Graceful shutdown
Fastify's close() drains connections. Wire it to SIGTERM:
for (const sig of ['SIGTERM', 'SIGINT']) {
process.on(sig, async () => {
await app.close();
process.exit(0);
});
}Step 4: Wire a managed Postgres
Use the official @fastify/postgres plugin, which manages a pool for you. It reads a connection string — DATABASE_URL, injected by PandaStack when you attach a managed PostgreSQL:
import fastifyPostgres from '@fastify/postgres';
app.register(fastifyPostgres, { connectionString: process.env.DATABASE_URL });
app.get('/users/:id', async (req) => {
const { rows } = await app.pg.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
return rows[0];
});Set the pool max below your tier's connection limit (50 free, 300 Pro, 1000 Premium).
Step 5: Structured logging
Fastify uses Pino, which emits JSON logs to stdout — perfect for the platform's live log view. In production, set the log level via env:
const app = Fastify({ logger: { level: process.env.LOG_LEVEL || 'info' } });Keep logs as JSON in production (don't use pino-pretty), so they're parseable downstream.
Step 6: Containerize
FROM node:20.18-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]Or skip the Dockerfile and let PandaStack's buildpacks auto-detect Node — both work.
Step 7: Deploy
Connect your repo and push:
git push origin mainThe build runs in a rootless BuildKit K8s Job pod, the image ships to Artifact Registry, and Helm deploys it with live logs streaming. Add a custom domain for automatic SSL, set env vars in the dashboard, and you're live.
Step 8: Scaling and health
- Fastify is single-threaded per process; add replicas for throughput rather than one giant instance.
- Your
/healthendpoint lets the orchestrator manage rollout and readiness. - On the free tier the app scales to zero on spot nodes (cold start after idle); paid tiers stay warm.
Production checklist
- [ ] JSON schema validation on all routes with bodies
- [ ] Binds to
$PORTon0.0.0.0 - [ ] Graceful
app.close()on SIGTERM - [ ]
@fastify/postgrespool sized to tier limits - [ ] JSON (non-pretty) Pino logs in production
- [ ]
/healthendpoint
References
- [Fastify documentation](https://fastify.dev/docs/latest/)
- [@fastify/postgres](https://github.com/fastify/fastify-postgres)
- [Pino logger](https://getpino.io/)
- [Fastify — validation and serialization](https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/)
---
Fastify gives you speed, validation, and great logging with very little boilerplate. Deploy one to PandaStack's [free tier](https://dashboard.pandastack.io), attach a managed Postgres, and let DATABASE_URL wire itself in — 5 web services and a database at $0/mo.