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

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.