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.