# 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.

- Autore / Author: Diego Betto (https://diegobetto.com/about/)
- Pubblicato / Published: 2026-08-27
- URL: https://diegobetto.com/fastify-rest-api-typescript/
- Tag: fastify, nodejs, typescript, api

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.

> 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

```bash
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:

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

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

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

## Il server minimo

```typescript
// 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`:

```typescript
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:

```bash
npm install @fastify/type-provider-typebox
```

```typescript
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**.

```typescript
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.

> 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`:

```typescript
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](https://diegobetto.com/nodejs-security-release-2026/) che vale la pena tenere d'occhio quando scegli su quale versione buildare in produzione.
