Usare PostgreSQL con Sequelize per creare un'API REST in ExpressJS

In questo articolo ti mostro come costruire un’API REST con ExpressJS, Sequelize e PostgreSQL.

Installazione


npm init -y
npm i express dotenv
npm i sequelize pg pg-hstore

Struttura consigliata

.
├─ src/
│  ├─ app.js
│  ├─ db.js
│  ├─ models/
│  │  └─ book.model.js
│  ├─ routes/
│  │  └─ books.routes.js
│  └─ middlewares/
│     └─ error.js
└─ .env

Variabili d’ambiente

# .env
DB_HOST=localhost
DB_PORT=5432
DB_NAME=library
DB_USER=postgres
DB_PASS=supersecret
NODE_ENV=development
PORT=3000

Connessione a PostgreSQL con Sequelize

// src/db.js
import { Sequelize } from "sequelize";
import dotenv from "dotenv";
dotenv.config();

export const sequelize = new Sequelize(
  process.env.DB_NAME,
  process.env.DB_USER,
  process.env.DB_PASS,
  {
  host: process.env.DB_HOST,
  port: Number(process.env.DB_PORT || 5432),
  dialect: "postgres",
  logging: process.env.NODE_ENV === "development" ? console.log : false,
  define: {
    underscored: true,
    paranoid: false,
    freezeTableName: false, // pluralizzazione automatica
    timestamps: true        // created_at / updated_at
  }
 }
);

export async function initDb() {
  try {
    await sequelize.authenticate();
    console.log("Connessione a PostgreSQL riuscita");
  } catch (err) {
    console.error("Connessione fallita:", err.message);
    process.exit(1);
  }
}

Definizione del modello

Esempio di risorsa Book con validazioni.

// src/models/book.model.js
import { DataTypes, Model } from "sequelize";
import { sequelize } from "../db.js";

export class Book extends Model {}

Book.init(
 {
  id: {
   type: DataTypes.BIGINT,
   autoIncrement: true,
    primaryKey: true
  },
  title: {
   type: DataTypes.STRING(200),
   allowNull: false,
   validate: {
     notEmpty: true,
     len: [1, 200]
   }
  },
  author: {
   type: DataTypes.STRING(120),
   allowNull: false
  },
  year: {
   type: DataTypes.INTEGER,
   allowNull: true,
    validate: {
     min: 0,
     max: new Date().getFullYear()
    }
  },
  price: {
   type: DataTypes.DECIMAL(10, 2),
   allowNull: true,
   validate: {
    min: 0
   }
  },
  inStock: {
    type: DataTypes.BOOLEAN,
    allowNull: false,
    defaultValue: true,
    field: "in_stock"
  }
 },
 {
  sequelize,
  tableName: "books",
  modelName: "Book",
  indexes: [
    { fields: ["title"] },
    { fields: ["author"] }
  ]
 }
);

Sincronizzazione schema (per sviluppo)

In sviluppo puoi usare sync per creare/aggiornare le tabelle. In produzione è consigliato usare migrazioni (vedi più avanti).

// src/app.js (bootstrap)
import express from "express";
import dotenv from "dotenv";
import { initDb, sequelize } from "./db.js";
import "./models/book.model.js"; // registra i modelli
import booksRouter from "./routes/books.routes.js";
import { errorHandler } from "./middlewares/error.js";

dotenv.config();
const app = express();
app.use(express.json());

app.get("/health", (_req, res) => res.json({ status: "ok" }));
app.use("/api/books", booksRouter);
app.use(errorHandler);

const port = Number(process.env.PORT || 3000);

const start = async () => {
  await initDb();
  if (process.env.NODE_ENV !== "production") {
    await sequelize.sync({ alter: true }); // Solo in sviluppo
  }
  app.listen(port, () => {
    console.log(`Server su http://localhost:${port}`);
  });
};

start();

Router RESTful


// src/routes/books.routes.js
import {
    Router
} from "express";
import {
    Op
} from "sequelize";
import {
    Book
} from "../models/book.model.js";

const router = Router();

/**

* GET /api/books
* Paginazione, filtri, ordinamento:
* * ?q=string (cerca su titolo/autore)
* * ?author=exact
* * ?minYear=1900&maxYear=2025
* * ?inStock=true|false
* * ?sort=year,-price  (prefisso - = desc)
* * ?page=1&limit=20
    */
router.get("/", async (req, res, next) => {
    try {
        const {
            q,
            author,
            minYear,
            maxYear,
            inStock,
            sort = "id",
            page = "1",
            limit = "10"
        } = req.query;

        const where = {};
        if (q) {
            where[Op.or] = [{
                    title: {
                        [Op.iLike]: `%${q}%`
                    }
                },
                {
                    author: {
                        [Op.iLike]: `%${q}%`
                    }
                }
            ];
        }
        if (author) where.author = author;
        if (minYear || maxYear) {
            where.year = {};
            if (minYear) where.year[Op.gte] = Number(minYear);
            if (maxYear) where.year[Op.lte] = Number(maxYear);
        }
        if (inStock !== undefined) where.inStock = inStock === "true";

        const order = sort.split(",").map(s => {
            if (s.startsWith("-")) return [s.slice(1), "DESC"];
            return [s, "ASC"];
        });

        const pageNum = Math.max(1, parseInt(page, 10) || 1);
        const limitNum = Math.min(100, Math.max(1, parseInt(limit, 10) || 10));
        const offset = (pageNum - 1) * limitNum;

        const {
            rows,
            count
        } = await Book.findAndCountAll({
            where,
            order,
            limit: limitNum,
            offset
        });

        res.json({
            data: rows,
            meta: {
                total: count,
                page: pageNum,
                pages: Math.ceil(count / limitNum),
                limit: limitNum
            }
        });
    } catch (err) {
        next(err);
    }
});

/** GET /api/books/:id */
router.get("/:id", async (req, res, next) => {
    try {
        const book = await Book.findByPk(req.params.id);
        if (!book) return res.status(404).json({
            error: "Not found"
        });
        res.json(book);
    } catch (err) {
        next(err);
    }
});

/** POST /api/books */
router.post("/", async (req, res, next) => {
    try {
        const book = await Book.create(req.body);
        res.status(201).json(book);
    } catch (err) {
        next(err);
    }
});

/** PUT /api/books/:id (sostituisce) */
router.put("/:id", async (req, res, next) => {
    try {
        const book = await Book.findByPk(req.params.id);
        if (!book) return res.status(404).json({
            error: "Not found"
        });
        await book.update(req.body);
        res.json(book);
    } catch (err) {
        next(err);
    }
});

/** PATCH /api/books/:id (parziale) */
router.patch("/:id", async (req, res, next) => {
    try {
        const book = await Book.findByPk(req.params.id);
        if (!book) return res.status(404).json({
            error: "Not found"
        });
        await book.update(req.body, {
            fields: Object.keys(req.body)
        });
        res.json(book);
    } catch (err) {
        next(err);
    }
});

/** DELETE /api/books/:id */
router.delete("/:id", async (req, res, next) => {
    try {
        const book = await Book.findByPk(req.params.id);
        if (!book) return res.status(404).json({
            error: "Not found"
        });
        await book.destroy();
        res.status(204).end();
    } catch (err) {
        next(err);
    }
});

export default router;

Middleware di gestione errori

// src/middlewares/error.js
export function errorHandler(err, _req, res, _next) {
  // Errori di validazione Sequelize
  if (err.name === "SequelizeValidationError" || err.name === "SequelizeUniqueConstraintError") {
    const details = err.errors?.map(e => ({ field: e.path, message: e.message })) || [];
    return res.status(400).json({ error: "ValidationError", details });
  }
  // Altri errori
  console.error(err);
  res.status(500).json({ error: "InternalServerError" });
}

Sequelize CLI e Migrazioni (consigliato in produzione)

Per controllare lo schema nel tempo usa il CLI.

npm i -D sequelize-cli

# Inizializza la struttura delle migrazioni

npx sequelize-cli init

# Crea una migrazione

npx sequelize-cli migration:generate --name create-books

# Applica le migrazioni

npx sequelize-cli db:migrate

# Rollback

npx sequelize-cli db:migrate:undo

Pratiche consigliate

  • Validazioni: definisci regole nel modello e lato API.
  • DTO/Schema: usa librerie di validazione (es. zod/joi) per la richiesta.
  • Perf: aggiungi indici dove filtri/ordini spesso.
  • Prod: usa migrazioni, SSL al database e variabili d’ambiente sicure.
  • Osservabilità: log strutturati e metriche sugli errori.

Conclusione

Con ExpressJS, Sequelize e PostgreSQL puoi creare rapidamente API REST robuste: definisci i modelli, gestisci paginazione/filtri/ordinamenti, applica validazioni e adotta migrazioni per ambienti reali. Espandi questo esempio con autenticazione, autorizzazione, caching e test automatici per una soluzione di produzione.

Torna su