Diego Betto's Blog
Logo di Fastify, il framework web per Node.js

27 agosto 2026 · 6 min di lettura

Creare una REST API con Fastify e TypeScript

Guida pratica a Fastify con TypeScript: routing, validazione con schema e architettura a plugin, per costruire una REST API veloce e tipizzata da zero.

Condividi:XLinkedInFacebookWhatsApp

Per anni, quasi ogni progetto Node.js che ho scritto partiva da npx express-generator. Poi mi sono trovato a dover ottimizzare un’API che doveva reggere qualche migliaio di richieste al secondo, e ho scoperto che gran parte del tempo se ne andava nel posto meno interessante possibile: il parsing e la validazione del body delle richieste, fatti a mano con express-validator sparso su venti route diverse.

Fastify nasce esattamente per questo problema. Non è “Express ma più veloce” — anche se lo è, e di parecchio — è un framework che tratta schema e validazione come cittadini di prima classe invece che come un plugin aggiunto dopo.

Perché Fastify e non Express

Tre differenze contano davvero, il resto è marketing:

  1. Gli schema non servono solo a validare l’input, servono anche a serializzare l’output. Fastify usa fast-json-stringify per generare una funzione di serializzazione JSON su misura per lo schema di risposta di ogni rotta — è più veloce di JSON.stringify() generico, e in un’API che risponde con payload grandi la differenza si sente.
  2. L’architettura è a plugin con incapsulamento, non un unico oggetto app globale su cui tutti montano middleware in ordine sparso. Ne parlo più sotto, è la parte che i tutorial veloci saltano quasi sempre.
  3. La validazione è dichiarativa, non imperativa: descrivi la forma dei dati con uno schema, Fastify genera il validatore a build time invece di eseguire una catena di controlli if ad ogni richiesta.

💡 Consiglio

Quando Express resta la scelta giusta: se il progetto è già scritto in Express e funziona, riscriverlo per qualche punto percentuale di throughput quasi mai vale il costo. Fastify conviene soprattutto per progetti nuovi, o per servizi dove la validazione dell’input è centrale (API pubbliche, microservizi ad alto traffico).

Setup del progetto

mkdir la-mia-api && cd la-mia-api
npm init -y
npm install fastify
npm install -D typescript tsx @types/node
npx tsc --init

Nel tsconfig.json generato, le opzioni che contano per Fastify sono queste:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "outDir": "dist"
  }
}

E due script in package.json per lo sviluppo e la build:

{
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js"
  }
}

Il server minimo

// src/server.ts
import Fastify from 'fastify';

const app = Fastify({ logger: true });

app.get('/health', async () => {
  return { status: 'ok' };
});

app.listen({ port: 3000 }, (err) => {
  if (err) {
    app.log.error(err);
    process.exit(1);
  }
});

logger: true non è decorativo: Fastify usa pino internamente, quindi ottieni logging strutturato JSON pronto per la produzione senza aggiungere nulla.

Validare (e serializzare) con gli schema

Qui inizia la parte che distingue davvero Fastify. Ogni rotta accetta un oggetto schema con body, params, querystring e response:

app.post('/users', {
  schema: {
    body: {
      type: 'object',
      required: ['email', 'name'],
      properties: {
        email: { type: 'string', format: 'email' },
        name: { type: 'string', minLength: 2 },
      },
    },
    response: {
      201: {
        type: 'object',
        properties: {
          id: { type: 'string' },
          email: { type: 'string' },
          name: { type: 'string' },
        },
      },
    },
  },
}, async (request, reply) => {
  const { email, name } = request.body as { email: string; name: string };
  const user = await createUser({ email, name });
  reply.code(201);
  return user;
});

Se il body non rispetta lo schema, Fastify risponde 400 automaticamente, prima ancora che il tuo handler venga chiamato. E lo schema.response non è solo documentazione: Fastify lo usa per costruire il serializzatore veloce di cui parlavo sopra — qualunque proprietà che restituisci ma non è nello schema viene silenziosamente esclusa dalla risposta, il che è anche un’ottima rete di sicurezza contro fughe accidentali di dati (pensa a un campo passwordHash che dimentichi di escludere a mano).

Tipizzare le rotte con TypeScript

Scrivere request.body as { email: string; name: string } funziona ma non è tipizzazione reale — è solo dire a TypeScript “fidati”. Per avere l’inferenza vera, il modo più diretto è usare @fastify/type-provider-typebox, che genera i tipi TypeScript direttamente dagli stessi schema JSON che già usi per la validazione:

npm install @fastify/type-provider-typebox
import { Type, type Static } from '@sinclair/typebox';
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';

const app = Fastify().withTypeProvider<TypeBoxTypeProvider>();

const CreateUserBody = Type.Object({
  email: Type.String({ format: 'email' }),
  name: Type.String({ minLength: 2 }),
});
type CreateUserBody = Static<typeof CreateUserBody>;

app.post('/users', {
  schema: { body: CreateUserBody },
}, async (request) => {
  // request.body è già tipizzato come CreateUserBody, nessun "as" necessario
  const { email, name } = request.body;
  return createUser({ email, name });
});

Un solo schema, due usi: valida a runtime e genera i tipi a compile time. Niente drift tra “quello che dico a TypeScript” e “quello che controllo davvero”.

L’architettura a plugin: l’idea che cambia il modo di organizzare il codice

In Express, tipicamente hai un app globale su cui monti middleware e router in un ordine che conta, e ogni file che importa app può aggiungere qualunque cosa a quell’oggetto condiviso. Fastify parte da un’idea diversa: ogni plugin ha il proprio scope, incapsulato.

import type { FastifyPluginAsync } from 'fastify';

const usersPlugin: FastifyPluginAsync = async (fastify) => {
  // fastify qui è un'istanza figlia, isolata da quella del chiamante
  fastify.decorateRequest('currentUser', null);

  fastify.addHook('preHandler', async (request) => {
    request.currentUser = await getUserFromToken(request.headers.authorization);
  });

  fastify.get('/me', async (request) => {
    return request.currentUser;
  });
};

app.register(usersPlugin, { prefix: '/users' });

Il decoratore currentUser e l’hook preHandler registrati dentro usersPlugin esistono solo dentro quel plugin e i suoi figli, non trapelano nel resto dell’app. Puoi registrare due plugin diversi con hook o decoratori omonimi senza che confliggano, perché vivono in scope separati — la stessa logica dei moduli ES, applicata alle rotte invece che ai file.

⚠ Attenzione

L’errore più comune: dimenticare await. fastify.register() è asincrono — se registri un plugin che decora fastify con qualcosa (un client database, un decoratore) e poi provi a usarlo subito dopo senza await, quel decoratore potrebbe non esistere ancora. In pratica: usa sempre await app.register(...) in sequenza, oppure lascia che Fastify gestisca l’ordine con fastify-plugin se il plugin deve esporre qualcosa al parent scope.

Un errore da matricola che vale la pena nominare

Se il tuo plugin deve decorare l’istanza parent (es. aggiungere un client Redis condiviso da tutta l’app, non solo da un sotto-scope), incapsulare senza volerlo il plugin lo rende invisibile al resto dell’app. La soluzione è fastify-plugin:

import fp from 'fastify-plugin';

export default fp(async (fastify) => {
  fastify.decorate('redis', createRedisClient());
});

fp() dice a Fastify “non incapsulare questo, fai trapelare i decoratori verso l’alto”. È il dettaglio che separa un plugin che sembra funzionare in isolamento da uno che si integra correttamente col resto dell’app.

E adesso?

Con routing, validazione e architettura a plugin hai le basi per un’API vera, non solo un endpoint giocattolo. I prossimi passi naturali sono l’autenticazione (Fastify ha @fastify/jwt e @fastify/cookie ufficiali) e la persistenza dati — se il tuo stack tocca anche la sicurezza delle dipendenze Node, ho scritto anche un riepilogo delle ultime security release di Node.js che vale la pena tenere d’occhio quando scegli su quale versione buildare in produzione.

Condividi:XLinkedInFacebookWhatsApp