DevTools

Cheatsheet Express.js

Framework web minimalista para Node.js

Back to languages
Express.js
105 cards found
Categories:
Versions:

Installation and Setup


10 cards
Installation
mkdir my-api && cd my-api
npm init -y
npm install express
npm install -D nodemon

npm init -y creates the package.json. express is the main dependency. nodemon (dev) restarts the server automatically when files change.

Separate app and server
// app.js
const app = express();
app.use(express.json());
// ... routes and middleware
module.exports = app;

// server.js
const app = require("./app");
const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Active na port ${port}`);
});

Separating configuration (app.js) from startup (server.js) makes testing easier — you can import app without opening a port. Recommended pattern for tests with supertest.

Environment variables
npm install dotenv

// server.js
require("dotenv").config();

const port = process.env.PORT || 3000;
const dbUrl = process.env.DB_URL;

// .env (do NOT commit)
PORT=3000
DB_URL=mongodb://localhost/app

dotenv loads variables from .env into process.env. Add .env to .gitignore. Use it for ports, DB URLs, secrets and API keys.

Basic server
const express = require("express");
const app = express();

app.get("/", (req, res) => {
  res.send("Hello Express!");
});

app.listen(3000, () => {
  console.log("Server em :3000");
});

express() creates the application. app.get() registers a route. app.listen() starts the HTTP server on the given port. The callback confirms when it is active.

ES Modules
// package.json
{ "type": "module" }

// app.js
import express from "express";
const app = express();

import { userRoutes } from "./routes/users.js";
app.use("/users", userRoutes);

export default app;

"type": "module" enables import/export (ESM) instead of require. Files need the .js extension in imports. Alternative: use .mjs.

Configuration per environment
const env = process.env.NODE_ENV || "development";

if (env === "production") {
  app.set("trust proxy", 1);
  app.use(helmet());
} else {
  app.use(morgan("dev"));
}

app.listen(port);

NODE_ENV distinguishes environments. In production: helmet, trust proxy, no verbose logs. In development: morgan("dev"), detailed errors. Set it via NODE_ENV=production node server.js.

Body parsers
// JSON (most common in APIs)
app.use(express.json());

// HTML forms
app.use(express.urlencoded({ extended: true }));

// Plain text
app.use(express.text());

// Size limit:
app.use(express.json({ limit: "5mb" }));

Without parsers, req.body is undefined. express.json() parses JSON. urlencoded for HTML forms. limit prevents huge payloads (DoS).

package.json scripts
{
  "scripts": {
    "dev": "nodemon src/server.js",
    "start": "node src/server.js",
    "test": "jest --watchAll"
  }
}

dev uses nodemon for development (auto-reload). start is for production (plain Node). test runs tests. Run with npm run dev, npm start, npm test.

Recommended structure
project/
  src/
    routes/       # settings de routes
    controllers/  # handler logic
    middleware/   # functions intermediate
    models/       # data access
    services/     # business logic
    app.js        # configuration da app
  server.js       # arranque do server
  package.json

Separation by responsibility: routes/ defines URLs, controllers/ processes requests, services/ holds the logic, models/ accesses the DB. Scalable and testable.

express-generator
npx express-generator my-app
cd my-app
npm install
npm start

// With EJS:
npx express-generator --view=ejs app

express-generator creates a complete structure with routes, views and preconfigured middleware. It includes morgan, cookie-parser and a template engine. Good to get started quickly.

Routes and Parameters


11 cards
HTTP methods
app.get("/users", list);
app.post("/users", create);
app.put("/users/:id", substituir);
app.patch("/users/:id", update);
app.delete("/users/:id", remove);

// All methods:
app.all("/route", handler);

Each HTTP verb has a corresponding method. GET (read), POST (create), PUT (replace), PATCH (partial update), DELETE (remove). all accepts any verb.

Chaining with route()
app.route("/books")
  .get((req, res) => {
    res.json(books);
  })
  .post((req, res) => {
    books.push(req.body);
    res.status(201).end();
  });

app.route() groups multiple verbs on the same route. It avoids repeating the path. Useful when GET and POST share the same endpoint. It also supports .put(), .delete(), etc.

Routes with sub-app
const admin = express();
admin.use(authAdmin);
admin.get("/dashboard", (req, res) => {
  res.send("Panel Admin");
});

// Mount on the main app:
app.use("/admin", admin);

An express() instance can be mounted the a sub-application. It inherits its own middleware. Useful for isolated sections with different configuration (e.g. admin, API v2).

Route parameters
app.get("/users/:id", (req, res) => {
  const id = req.params.id;
  res.send(`User ${id}`);
});

// Multiple parameters:
app.get("/stores/:lojaId/products/:prodId",
  (req, res) => {
    const { lojaId, prodId } = req.params;
  });

:id defines a dynamic segment in the URL. The value ends up in req.params.id. Multiple params are supported. Always strings — convert with parseInt() or Number() if needed.

Optional parameters and regex
// Optional (:suffix?)
app.get("/users/:id?", handler);

// Regex pattern
app.get("/files/:name(\\d+)", handler);

// Multiple paths:
app.get(["/a", "/b", "/c"], handler);

:id? makes the parameter optional. (\d+) restricts it to digits (inline regex). An array of paths registers the same handler on multiple URLs. Total flexibility in routing.

req.baseUrl and mount path
// app.use("/api/v1", router);

router.get("/users", (req, res) => {
  req.baseUrl;    // "/api/v1"
  req.path;       // "/users"
  req.originalUrl; // "/api/v1/users"
});

req.baseUrl is the prefix where the router was mounted. req.path is the path within the router. req.originalUrl is the full URL. Useful to generate absolute links.

Query string
// GET /search?q=node&page=2&limit=10
app.get("/search", (req, res) => {
  const q = req.query.q;         // "node"
  const page = req.query.page;   // "2"
  const limit = +req.query.limit || 10;
  res.json({ q, page, limit });
});

req.query contains the parameters after ? in the URL. Values are strings — use + or Number() to convert. Ideal for filters, pagination and sorting.

router.param()
router.param("id", async (req, res, next, id) => {
  const user = await User.findById(id);
  if (!user) {
    return res.status(404).json({ error: "Not found" });
  }
  req.user = user;
  next();
});

router.get("/:id", (req, res) => {
  res.json(req.user); // already carregado
});

router.param() runs middleware when a specific parameter appears. Ideal to load resources from the DB once and reuse them in all routes with :id.

Wildcard and 404
// Catch any undefined route:
app.use("*", (req, res) => {
  res.status(404).json({
    error: `Route ${req.originalUrl} not exists`
  });
});

// Must be the LAST middleware

* (or app.use with no path) catches everything not handled. It must be registered last. It returns 404 for nonexistent routes. In SPAs, it can serve index.html.

Modular router
// routes/users.js
const router = express.Router();

router.get("/", list);
router.post("/", create);
router.get("/:id", ver);
router.put("/:id", update);

module.exports = router;

// app.js
app.use("/users", require("./routes/users"));

express.Router() creates a mini-app of routes. app.use("/users", router) mounts it with a prefix. It keeps the main file clean. Each resource has its own routes file.

Router with middleware
const router = express.Router();

// Applies to all routes of this router:
router.use(authenticate);
router.use(logAcesso);

router.get("/", list);
router.post("/", create);

module.exports = router;

router.use() applies middleware only to the routes of that router. It does not affect other app routes. Ideal for section-specific auth (e.g. all /admin routes require admin).

Middleware


11 cards
Global middleware
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next();
});

app.use() with no path applies to all routes. next() passes to the next middleware. Without next(), the request hangs (pending). Register it before the routes.

next() and flow
app.use((req, res, next) => {
  req.user = { id: 1, name: "Anna" };
  next();
});

// Pass an error to the handler:
app.use((req, res, next) => {
  if (!req.query.token) {
    return next(new Error("Token ausente"));
  }
  next();
});

next() moves on to the next middleware. next(error) jumps directly to the error handler. You can add data to req to use in later handlers.

Essential third-party
const morgan = require("morgan");
const cors = require("cors");
const helmet = require("helmet");
const compression = require("compression");

app.use(morgan("dev"));
app.use(cors());
app.use(helmet());
app.use(compression());

morgan (logs), cors (cross-origin), helmet (secure headers), compression (gzip). The 4 most used middlewares in production. Register them at the top of the app.

Per-route middleware
function authenticate(req, res, next) {
  if (!req.headers.authorization) {
    return res.status(401).json({ error: "Not autorizado" });
  }
  next();
}

app.get("/profile", authenticate, handler);

Inline middleware runs before the final handler. You can chain multiple: app.get("/", auth, validate, handler). If it does not call next(), it must send a response.

Middleware with configuration
function rateLimit({ max, window }) {
  const orders = new Map();
  return (req, res, next) => {
    const ip = req.ip;
    const now = Date.now();
    // counting logic...
    if (excedeu) return res.status(429).end();
    next();
  };
}

app.use(rateLimit({ max: 100, window: 60000 }));

A function that returns middleware (factory pattern). It accepts configuration options. It allows reuse with different parameters. Pattern used by cors(), helmet(), etc.

Skip and mounting
// Morgan with skip:
app.use(morgan("dev", {
  skip: (req) => req.url.startsWith("/health")
}));

// Mount on a specific path:
app.use("/uploads", express.static("uploads"));

Some middlewares accept skip to ignore certain requests. app.use(path, middleware) mounts it only on a prefix. It reduces overhead on health check or static routes.

Execution order
// Order matters!
app.use(express.json());    // 1st - parse body
app.use(logger);            // 2nd - log
app.use("/api", routes);     // 3rd - routes
app.use(notFound);          // 4th - 404
app.use(tratadorDeErros);   // 5th - errors (last)

Middleware runs in the registration order via app.use(). Parsers before routes. 404 after routes. Error handler always at the end (4 arguments). Wrong order = unexpected behavior.

Conditional middleware
// Only in development:
if (process.env.NODE_ENV === "development") {
  app.use(morgan("dev"));
}

// Only for /api:
app.use("/api", express.json());

// Only for POST/PUT:
app.use((req, res, next) => {
  if (["POST", "PUT"].includes(req.method)) {
    return express.json()(req, res, next);
  }
  next();
});

Middleware can be applied conditionally by environment, path or method. app.use("/api", ...) limits it to a prefix. It avoids unnecessary processing.

res.locals
app.use((req, res, next) => {
  res.locals.anoAtual = new Date().getFullYear();
  res.locals.userLogado = req.user || null;
  next();
});

// In templates (EJS):
// <%= anoAtual %>
// In handlers:
app.get("/", (req, res) => {
  res.render("index", { year: res.locals.anoAtual });
});

res.locals stores data available in templates and later handlers. Different from req — it is specific to the response. Ideal for layout data (user, year, config).

Error middleware
// 4 required arguments
app.use((error, req, res, next) => {
  console.error(error.stack);
  const status = error.status || 500;
  res.status(status).json({
    error: error.message,
    ...(process.env.NODE_ENV !== "production" && {
      stack: error.stack
    }),
  });
});

The error handler has 4 parameters (err, req, res, next). It catches errors passed via next(error) or thrown. Never expose the stack in production. It must be the last middleware.

Async middleware
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

app.get("/users", asyncHandler(async (req, res) => {
  const users = await User.find();
  res.json(users);
}));

Express 4 does not catch async errors automatically. asyncHandler wraps the Promise and forwards rejections to next(). Express 5 catches them natively.

Request e Input


11 cards
req.body
// POST with JSON
app.post("/users", (req, res) => {
  const { name, email } = req.body;
  res.json({ name, email });
});

// Requires: app.use(express.json())

req.body contains the parsed request body. Only available after express.json() or urlencoded(). In GET it is usually undefined or {}.

Cookies
const cookieParser = require("cookie-parser");
app.use(cookieParser());

app.get("/", (req, res) => {
  // Read:
  const session = req.cookies.session;
  const assinado = req.signedCookies.token;

  // Set:
  res.cookie("theme", "escuro", {
    maxAge: 86400000, httpOnly: true
  });
});

cookie-parser parses the cookies. req.cookies for normal ones, req.signedCookies for signed ones. res.cookie() sets them with options: maxAge, httpOnly, secure.

req.route and matched
app.get("/users/:id", (req, res) => {
  req.route.path;    // "/users/:id"
  req.route.methods; // { get: true }
  req.baseUrl;       // prefix do router
});

req.route shows the route that matched. path is the pattern (with :id), methods the registered verbs. Useful for debugging and route logging.

req.params
// GET /users/42/posts/7
app.get("/users/:userId/posts/:postId", (req, res) => {
  req.params.userId;  // "42"
  req.params.postId;  // "7"

  // Destructuring:
  const { userId, postId } = req.params;
});

req.params contains the dynamic route segments. Values are always strings. Define them with :name in the route. Multiple params are supported in the same route.

Request properties
req.method       // "GET"
req.url          // "/users?page=2"
req.path         // "/users"
req.originalUrl  // "/api/users?page=2"
req.ip           // "127.0.0.1"
req.protocol     // "http" or "https"
req.secure       // true se HTTPS
req.xhr          // true se AJAX

Useful properties: method (verb), path (no query), ip (client), protocol, secure. req.xhr detects XMLHttpRequest requests.

Body with limit and types
// Limit size:
app.use(express.json({ limit: "1mb" }));

// Only for certain routes:
app.use("/api", express.json());

// Raw body (webhooks):
app.use("/webhook", express.raw({ type: "*/*" }));

limit prevents huge payloads (DoS attack). You can apply parsers only to certain paths. express.raw() keeps the body the a Buffer — needed to verify webhook signatures.

req.query
// GET /products?category=books&order=price&page=2
app.get("/products", (req, res) => {
  const { category, order } = req.query;
  const page = parseInt(req.query.page) || 1;

  // Arrays: ?tags=a&tags=b
  // req.query.tags = ["a", "b"]
});

req.query are the parameters after ?. Values are strings (or arrays if repeated). Ideal for filters, sorting and pagination. Always validate and sanitize.

req.is() and content-type
app.post("/upload", (req, res) => {
  if (req.is("json")) {
    // process JSON
  } else if (req.is("multipart/form-data")) {
    // process upload
  } else {
    res.status(415).json({ error: "Type not supported" });
  }
});

req.is(type) checks the request's Content-Type. It returns the type if it matches, false otherwise. 415 is the status for unsupported content type.

Sanitize input
const { body } = require("express-validator");

app.post("/users",
  body("name").trim().escape(),
  body("email").normalizeEmail(),
  (req, res) => {
    const name = req.body.name; // already limpo
  }
);

trim() removes spaces. escape() converts HTML entities (prevents XSS). normalizeEmail() standardizes emails. Always sanitize before saving to the DB or rendering.

req.headers
app.get("/info", (req, res) => {
  const auth = req.headers["authorization"];
  const type = req.get("Content-Type");
  const accept = req.accepts("json");
  const host = req.hostname;
  const ua = req.get("User-Agent");
  res.json({ auth, type, host, ua });
});

req.headers gives access to all headers (lowercase). req.get() is a shortcut for a specific header. req.accepts() checks the Accept header. Headers are case-insensitive.

req.files (multer)
const upload = multer({ dest: "uploads/" });

app.post("/photos", upload.array("photos", 5), (req, res) => {
  req.files.forEach(f => {
    console.log(f.originalname, f.size, f.mimetype);
  });
  res.json({ total: req.files.length });
});

With multer, req.files contains the uploaded files. Each file has originalname, size, mimetype, path. upload.array() accepts multiple.

Response e Output


11 cards
res.send()
res.send("text simple");
res.send({ ok: true });
res.send("<h1>HTML</h1>");
res.send(Buffer.from("binary"));

res.send() sends the response and sets Content-Type automatically. String → text/html. Object → application/json. Buffer → application/octet-stream. It ends the request-response cycle.

Response headers
res.set("X-Token", "abc123");
res.set("Cache-Control", "no-cache");
res.type("application/pdf");

// Multiple at once:
res.set({
  "X-API-Version": "2.0",
  "X-RateLimit-Remaining": "99",
});

res.set() sets headers on the response. res.type() is a shortcut for Content-Type. Headers must be set before send()/json(). Useful for tokens, rate limit info, versioning.

res.cookie()
res.cookie("session", token, {
  httpOnly: true,
  secure: true,
  maxAge: 7 * 24 * 60 * 60 * 1000,
  sameSite: "strict",
});

// Remove:
res.clearCookie("session");

res.cookie() sets cookies with options. httpOnly prevents access via JS (anti-XSS). secure only over HTTPS. sameSite prevents CSRF. clearCookie() removes it.

res.json()
res.json({ users: [], total: 0 });
res.status(201).json(novoUser);
res.status(404).json({ error: "Not found" });

// JSONP (legacy):
res.jsonp({ data: [] });

res.json() serializes to JSON and sets Content-Type: application/json. More explicit than send() for APIs. It accepts objects, arrays, null. jsonp adds a callback (avoid).

res.download() and sendFile()
const path = require("path");

// Download (Content-Disposition header):
res.download("./relatorios/2024.pdf", "report.pdf");

// Send file inline:
res.sendFile(path.join(__dirname, "public", "index.html"));

res.download() forces a download with a custom name. res.sendFile() serves the file inline (the browser displays it). Use path.join() for safe paths. It prevents path traversal.

res.location() and links
// Location header (no redirect):
res.location("/users/42");
res.status(201).json({ id: 42 });

// Link header (pagination):
res.links({
  next: "/users?page=3",
  last: "/users?page=10",
});

res.location() sets the Location header without redirecting. res.links() sets the Link header (REST pagination). Standard in APIs: 201 + Location for a created resource.

Status codes
res.status(200).json({ ok: true });   // success
res.status(201).json(new);           // created
res.status(204).end();                // without content
res.status(400).json({ error });       // invalid
res.status(401).json({ error });       // not autenticado
res.status(403).json({ error });       // without permission
res.status(404).json({ error });       // not found
res.status(500).json({ error });       // error server

res.status() sets the HTTP code (chainable). 2xx success, 4xx client error, 5xx server error. 204 has no body — use .end().

res.render() (templates)
// Configure engine:
app.set("view engine", "ejs");
app.set("views", "./src/views");

// Render:
app.get("/", (req, res) => {
  res.render("index", {
    title: "Start",
    users: listaUsers,
  });
});

res.render() compiles a template with data and sends HTML. It supports EJS, Pug, Handlebars. view engine defines the engine. views defines the folder. The data is available in the template.

res.append() and attachment()
// Add a header without overwriting:
res.append("Set-Cookie", "a=1");
res.append("Set-Cookie", "b=2");

// Force download with a name:
res.attachment("photo.png");
res.sendFile("./uploads/photo.png");

res.append() adds values to an existing header (does not replace). res.attachment() sets Content-Disposition for download. Useful for multiple cookies or to force a download.

res.redirect()
res.redirect("/login");
res.redirect(301, "/new-url");    // permanent
res.redirect(302, "/temporary");  // temporary
res.redirect("back");              // back (Referer)

// After creating a resource:
res.redirect(`/users/${novoUser.id}`);

res.redirect() sends HTTP 302 by default. 301 for a permanent redirect (SEO). "back" uses the Referer header. Common after POST to avoid re-submission (PRG pattern).

res.end() and streaming
// No body:
res.status(204).end();

// Streaming (large data):
app.get("/export", (req, res) => {
  res.set("Content-Type", "text/csv");
  const stream = fs.createReadStream("data.csv");
  stream.pipe(res);
});

res.end() closes with no body. stream.pipe(res) sends data in chunks (efficient for large files). It does not load everything into memory. Ideal for exports, videos, large downloads.

REST API is CRUDE


11 cards
Complete CRUD
let users = [];

app.get("/users", (req, res) => res.json(users));

app.post("/users", (req, res) => {
  const u = { id: Date.now(), ...req.body };
  users.push(u);
  res.status(201).json(u);
});

app.put("/users/:id", (req, res) => {
  const i = users.findIndex(u => u.id == req.params.id);
  if (i === -1) return res.status(404).end();
  users[i] = { ...users[i], ...req.body };
  res.json(users[i]);
});

app.delete("/users/:id", (req, res) => {
  users = users.filter(u => u.id != req.params.id);
  res.status(204).end();
});

REST CRUD: GET (list), POST (create, 201), PUT (replace), DELETE (remove, 204). 404 if the resource does not exist. Standard for any API.

API versioning
// By URL (most common):
app.use("/api/v1", rotasV1);
app.use("/api/v2", rotasV2);

// By header:
app.use((req, res, next) => {
  const version = req.get("API-Version") || "1";
  req.apiVersion = version;
  next();
});

URL versioning (/api/v1) is the simplet and most explicit. By header it is cleaner but less visible. Keep backward compatibility. Document breaking changes.

Idempotency key
const processados = new Map();

app.post("/payments", (req, res) => {
  const key = req.get("Idempotency-Key");
  if (processados.has(key)) {
    return res.json(processados.get(key));
  }

  const result = processPayment(req.body);
  processados.set(key, result);
  res.status(201).json(result);
});

Idempotency-Key avoids duplicate processing. If the key was already seen, it returns the previous response. Essential for payments and non-idempotent operations. The client generates the key (UUID).

Pagination
app.get("/users", (req, res) => {
  const page = Math.max(1, +req.query.page || 1);
  const limit = Math.min(100, +req.query.limit || 10);
  const start = (page - 1) * limit;

  const items = users.slice(start, start + limit);
  res.json({
    data: items,
    meta: { total: users.length, page, limit },
  });
});

Pagination with page and limit via query params. Math.max/Math.min prevent invalid values. Return meta with the total so the client can calculate pages.

HATEOAS and links
app.get("/users/:id", (req, res) => {
  const user = users.find(u => u.id == req.params.id);
  res.json({
    ...user,
    _links: {
      self: `/users/${user.id}`,
      posts: `/users/${user.id}/posts`,
      delete: `/users/${user.id}`,
    },
  });
});

HATEOAS includes links in the response for resource discovery. _links indicates available actions. It makes the API self-descriptive. Standard in mature REST APIs.

API with controller
// controllers/userController.js
exports.list = async (req, res, next) => {
  try {
    const users = await User.find();
    res.json(users);
  } catch (e) { next(e); }
};

exports.create = async (req, res, next) => {
  try {
    const user = await User.create(req.body);
    res.status(201).json(user);
  } catch (e) { next(e); }
};

Controllers separate the logic from the handlers. exports.method for each action. try/catch with next(e) forwards errors. It keeps routes clean and code testable.

Filters and sorting
app.get("/products", (req, res) => {
  let result = [...products];

  if (req.query.category) {
    result = result.filter(
      p => p.category === req.query.category
    );
  }

  const order = req.query.order === "desc" ? -1 : 1;
  result.sort((a, b) => (a.price - b.price) * order);

  res.json(result);
});

Filters via query params (?category=x). Sorting with ?order=desc. Combine with pagination for complete APIs. In a real DB, use WHERE and ORDER BY.

Partial PATCH
app.patch("/users/:id", (req, res) => {
  const user = users.find(u => u.id == req.params.id);
  if (!user) return res.status(404).end();

  // Update only AS sent fields:
  const allowedFields = ["name", "email", "active"];
  allowedFields.forEach(field => {
    if (req.body[field] !== undefined) {
      user[field] = req.body[field];
    }
  });

  res.json(user);
});

PATCH updates partially (only the sent fields). PUT replaces everything. A field whitelist prevents updating id or role. Always validate the received fields.

Service layer
// services/userService.js
class UserService {
  async list(filters) {
    return User.find(filters).lean();
  }

  async create(data) {
    if (await this.emailExiste(data.email)) {
      throw new ApiError(409, "Email already exists");
    }
    return User.create(data);
  }
}

module.exports = new UserService();

Services hold the business rules (validations, logic). Controllers only orchestrate (receive, call the service, respond). Clear separation: route → controller → service → model. Maximum testability with a per-class exports.

Standardized responses
// Success:
res.json({
  success: true,
  data: users,
  meta: { total: 50 },
});

// Error:
res.status(400).json({
  success: false,
  error: "Email already exists",
  fields: { email: "Deve be unique" },
});

Consistent structure: success (boolean), data (payload), error (message), fields (per-field errors). It makes parsing easier on the frontend. Document the format.

Bulk operations
app.post("/users/bulk", (req, res) => {
  const newItems = req.body.users;
  if (!Array.isArray(newItems)) {
    return res.status(400).json({ error: "Array esperado" });
  }

  const created = newItems.map(u => ({ id: Date.now(), ...u }));
  users.push(...created);
  res.status(201).json({ created: created.length });
});

Bulk operations to create/update/remove multiple resources via POST. Validate that it is an array. Limit the size (e.g. max 100). Return an operation summary. Useful for imports and syncs.

Advanced Features


10 cards
Upload with multer
const multer = require("multer");

const storage = multer.diskStorage({
  destination: "./uploads/",
  filename: (req, file, cb) => {
    cb(null, `${Date.now()}-${file.originalname}`);
  },
});

const upload = multer({
  storage,
  limits: { fileSize: 5 * 1024 * 1024 },
  fileFilter: (req, file, cb) => {
    cb(null, file.mimetype.startsWith("image/"));
  },
});

app.post("/upload", upload.single("photo"), (req, res) => {
  res.json({ path: req.file.path });
});

multer processes multipart/form-data. diskStorage controls the destination and name. limits restricts the size. fileFilter validates the type. single(), array(), fields() for different uses.

Scheduling (node-cron)
const cron = require("node-cron");

// Every day at 3am:
cron.schedule("0 3 * * *", async () => {
  console.log("Clearing expired tokens...");
  await Token.deleteMany({ expira: { $lt: new Date() } });
});

// Every 5 minutes:
cron.schedule("*/5 * * * *", () => {
  verificarServicos();
});

node-cron schedules recurring tasks in the Express process. Standard cron syntax (min hour day month weekday). Ideal for cleanup, reports, syncs. For heavy production, use Bull/BullMQ.

Cluster mode
const cluster = require("cluster");
const the = require("the");

if (cluster.isPrimary) {
  const cpus = os.cpus().length;
  for (let i = 0; i < cpus; i++) {
    cluster.fork();
  }
  cluster.on("exit", () => cluster.fork());
} else {
  require("./server");
}

cluster creates one process per CPU. The primary distributes connections. If a worker dies, another is created. It takes advantage of multi-core. A modern alternative: use PM2 with -i max.

WebSockets (Socket.IO)
const http = require("http");
const { Server } = require("socket.io");

const server = http.createServer(app);
const io = new Server(server);

io.on("connection", (socket) => {
  socket.on("message", (data) => {
    io.emit("message", data);
  });
  socket.on("disconnect", () => {});
});

server.listen(3000);

Socket.IO adds WebSockets to Express. io.on("connection") when a client connects. socket.emit() sends to one. io.emit() to everyone. Ideal for chat, real-time notifications.

Server-Sent Events
app.get("/events", (req, res) => {
  res.set({
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });

  const id = setInterval(() => {
    res.write(`data: ${JSON.stringify({ hour: new Date() })}\n\n`);
  }, 1000);

  req.on("close", () => clearInterval(id));
});

SSE sends events from the server to the client (one-way). text/event-stream header. res.write() sends without closing. Simpler than WebSockets for notifications and feeds.

Health check
app.get("/health", async (req, res) => {
  const checks = {
    uptime: process.uptime(),
    bd: "ok",
    redis: "ok",
  };

  try {
    await mongoose.connection.db.admin().ping();
  } catch {
    checks.bd = "error";
    return res.status(503).json(checks);
  }

  res.json(checks);
});

/health checks whether the service is operational. It tests connections (DB, Redis, APIs). It returns 503 if something fails. Used by load balancers and Kubernetes for liveness/readiness probes.

EJS templates
npm install ejs

// app.js
app.set("view engine", "ejs");

// views/index.ejs
// <h1><%= title %></h1>
// <% users.forEach(u => { %>
//   <p><%= u.name %></p>
// <% }) %>

app.get("/", (req, res) => {
  res.render("index", { title: "Home", users });
});

EJS is the simplet template engine. <%= %> prints (escaped). <% %> runs logic. res.render() compiles with data. Include partials with <%- include() %>.

Reverse proxy
const { createProxyMiddleware } = require("http-proxy-middleware");

// Forward /api/legacy to another service:
app.use("/api/legacy", createProxyMiddleware({
  target: "http://service-old:4000",
  changeOrigin: true,
  pathRewrite: { "^/api/legacy": "" },
}));

http-proxy-middleware forwards requests to other services. target is the destination. pathRewrite removes the prefix. Useful for gradual migrations, microservices and external APIs.

Caching with Redis
const Redis = require("ioredis");
const redis = new Redis();

async function cacheMiddleware(req, res, next) {
  const key = `cache:${req.originalUrl}`;
  const cached = await redis.get(key);
  if (cached) return res.json(JSON.parse(cached));

  const originalJson = res.json.bind(res);
  res.json = (data) => {
    redis.setex(key, 60, JSON.stringify(data));
    return originalJson(data);
  };
  next();
}

app.get("/api/products", cacheMiddleware, handler);

Redis caches responses by URL with a TTL (60s). It intercepts res.json() to store. If the cache exists, it responds without going to the DB. It drastically reduces load on frequently read endpoints.

Graceful shutdown
const server = app.listen(3000);

process.on("SIGTERM", () => {
  console.log("Encerrando...");
  server.close(() => {
    mongoose.connection.close();
    redis.quit();
    process.exit(0);
  });

  // Force after 10s:
  setTimeout(() => process.exit(1), 10000);
});

SIGTERM is sent by Docker/the orchestrator. server.close() stops accepting connections and waits for active ones to finish. Close DB/Redis connections. The timeout forces shutdown if something blocks.

Good Practices and Tools


10 cards
Logging with morgan
const morgan = require("morgan");

// Development (colored):
app.use(morgan("dev"));
// GET /users 200 3.2ms - 1.2kb

// Production (detailed):
app.use(morgan("combined"));

// To a file:
const fs = require("fs");
app.use(morgan("combined", {
  stream: fs.createWriteStream("access.log", { flags: "a" })
}));

morgan logs every HTTP request. "dev" is colored and concise. "combined" is Apache format (for production). stream redirects to a file. Combine with Winston for app logs.

Docker
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

# .dockerignore
node_modules
.env

node:20-alpine is the lightweight base image. npm ci installs exactly (lock file). .dockerignore excludes node_modules and .env. EXPOSE documents the port. Multi-stage for a smaller build.

Production checklist
// ✓ NODE_ENV=production
// ✓ helmet() + cors() configured
// ✓ Rate limiting active
// ✓ Body com limit
// ✓ HTTPS forced
// ✓ Structured logs (Winston)
// ✓ Graceful shutdown
// ✓ Health check endpoint
// ✓ No secrets in the code
// ✓ PM2 or Docker
// ✓ Tests passing

Before deploy: security (helmet, cors, limits), observability (logs, health), resilience (graceful shutdown, PM2), and hygiene (no secrets, tests). Review this list on every release.

Winston (structured logs)
const winston = require("winston");

const logger = winston.createLogger({
  level: "info",
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: "error.log", level: "error" }),
    new winston.transports.File({ filename: "app.log" }),
    new winston.transports.Console({ format: winston.format.simple() }),
  ],
});

logger.info("Server iniciado", { port: 3000 });
logger.error("Failure BD", { error: e.message });

Winston is the most popular logger. Levels: error, warn, info, debug. Multiple transports (console, file, services). JSON format for production (parseable by ELK, Datadog).

Organizing by feature
src/
  features/
    users/
      user.routes.js
      user.controller.js
      user.service.js
      user.model.js
      user.test.js
    posts/
      post.routes.js
      post.controller.js
  shared/
    middleware/
    utils/
  app.js

Organizing by feature groups everything for a resource together. More scalable than separating by type (routes/, controllers/). Easy to find and remove features. shared/ for common code.

Express 5 (what is new)
npm install express@5

// Async errors caught natively:
app.get("/users", async (req, res) => {
  const users = await User.find(); // reject → 500 automatic
  res.json(users);
});

// Wildcard changed:
app.get("/users/{id}", handler);  // instead of :id
app.use("/*splat", notFound);     // instead of *

Express 5 catches async errors without a wrapper. Route syntax changed: {id} instead of :id, *splat instead of *. Native promises in res.redirect(). Minimal breaking changes.

Testing with supertest
const request = require("supertest");
const app = require("./app");

describe("API Users", () => {
  it("GET /users returns list", async () => {
    const res = await request(app)
      .get("/users")
      .expect(200)
      .expect("Content-Type", /json/);

    expect(res.body).toBeInstanceOf(Array);
  });

  it("POST /users creates", async () => {
    const res = await request(app)
      .post("/users")
      .send({ name: "Anna", email: "ana@test.com" })
      .expect(201);
  });
});

supertest tests endpoints without opening a port. request(app) simulates HTTP requests. .expect() checks status and headers. .send() sends the body. Combine with Jest or Mocha. It tests the whole app (integration).

Dependency injection
// The controller receives the service (testable):
function createUserController(userService) {
  return {
    async list(req, res) {
      const users = await userService.list();
      res.json(users);
    },
    async create(req, res) {
      const user = await userService.create(req.body);
      res.status(201).json(user);
    },
  };
}

// Injection:
const ctrl = createUserController(new UserService());

Factory functions receive dependencies the arguments via require. It makes testing with mocks easier. No coupling to concrete implementations. Alternative: use a DI container (tsyringe, awilix).

PM2 (process manager)
npm install -g pm2

pm2 start server.js -i max
pm2 list
pm2 logs
pm2 restart all
pm2 save
pm2 startup

PM2 manages Node processes in production. -i max uses all CPUs (cluster). Auto-restart on crash. pm2 logs shows the output. pm2 startup starts on boot. Monitor with pm2 monit.

Centralized config
// config/index.js
require("dotenv").config();

module.exports = {
  port: process.env.PORT || 3000,
  dbUrl: process.env.DB_URL,
  jwtSecret: process.env.JWT_SECRET,
  jwtExpiry: "24h",
  cors: {
    origin: process.env.CORS_ORIGIN || "*",
  },
  isProduction: process.env.NODE_ENV === "production",
};

Centralize all configuration in one module. A single point for environment variables. Sensible defaults for development. Validate that secrets exist at startup. Never scatter process.env throughout the code.

Validation and Errors


10 cards
Manual validation
function validarUser(req, res, next) {
  const { name, email } = req.body;
  const errors = [];

  if (!name || name.trim().length < 2)
    errors.push("Name must have 2+ characters");
  if (!email || !email.includes("@"))
    errors.push("Email invalid");

  if (errors.length)
    return res.status(400).json({ errors });
  next();
}

app.post("/users", validarUser, create);

A validation middleware checks fields before the handler. It returns 400 with a list of errors if invalid. next() only if everything passes. Simple but repetitive for many fields.

Custom error class
class ApiError extends Error {
  constructor(status, message, fields = {}) {
    super(message);
    this.status = status;
    this.fields = fields;
    this.isOperacional = true;
  }
}

// Usage:
throw new ApiError(404, "User not found");
throw new ApiError(422, "Error de validation", {
  email: "Already exists"
});

ApiError standardizes errors with a status and fields. isOperacional distinguishes expected errors from bugs. The global handler formats the response. It avoids repeating res.status().json() everywhere.

notFound middleware
// After all routes:
app.use((req, res) => {
  res.status(404).json({
    success: false,
    error: `Route ${req.method} ${req.originalUrl} not exists`,
  });
});

// For an SPA (serve index.html):
app.get("*", (req, res) => {
  res.sendFile(path.join(__dirname, "public", "index.html"));
});

The 404 middleware catches undefined routes. For APIs, return JSON. For SPAs, serve index.html (client-side routing). It must come after all routes but before the error handler.

express-validator
const { body, validationResult } = require("express-validator");

app.post("/users",
  body("name").notEmpty().isLength({ min: 2 }),
  body("email").isEmail().normalizeEmail(),
  body("age").optional().isInt({ min: 18 }),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty())
      return res.status(400).json({ errors: errors.array() });
    // continue...
  }
);

express-validator validates declaratively. body(), param(), query() for each source. validationResult() collects errors. It supports optional(), sanitization and custom messages.

Global error handler
app.use((error, req, res, next) => {
  const status = error.status || 500;
  const response = {
    success: false,
    error: error.message,
    ...(error.fields && { fields: error.fields }),
  };

  if (process.env.NODE_ENV !== "production") {
    response.stack = error.stack;
  }

  res.status(status).json(response);
});

A single handler formats all errors. Operational errors show the message. Bugs (500) hide details in production. stack only in development. Always the last middleware.

try/catch in controllers
exports.create = async (req, res, next) => {
  try {
    const user = await UserService.create(req.body);
    res.status(201).json(user);
  } catch (error) {
    if (error.code === 11000) {
      return res.status(409).json({
        error: "Email already registado"
      });
    }
    next(error);
  }
};

try/catch lets you handle specific errors (e.g. duplicate 11000) and forward the rest with next(error). More control than plain asyncHandler. Combine both for maximum coverage.

Validation with Zod
const { z } = require("zod");

const userSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  age: z.number().min(18).optional(),
});

app.post("/users", (req, res) => {
  const result = userSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({
      errors: result.error.issues
    });
  }
  const data = result.data; // typed
});

Zod validates and parses with types. safeParse() does not throw an exception. result.data is the validated and transformed input. More modern than express-validator. It supports complex schemas.

Async errors (Express 4)
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

// Usage:
app.get("/users/:id", asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) throw new ApiError(404, "Not found");
  res.json(user);
}));

Express 4 does not catch rejects from async. asyncHandler wraps and forwards to next(). In Express 5, async errors are caught natively — no wrapper needed.

Validating params and query
const { param, query } = require("express-validator");

app.get("/users/:id",
  param("id").isMongoId(),
  query("page").optional().isInt({ min: 1 }),
  handler
);

// With Zod:
const paramsSchema = z.object({ id: z.string().uuid() });
const querySchema = z.object({ page: z.coerce.number().default(1) });

Also validate params and query — not just the body. param("id").isMongoId() prevents invalid DB queries. z.coerce.number() converts a string to a number automatically.

Mongoose validation errors
app.use((error, req, res, next) => {
  if (error.name === "ValidationError") {
    const fields = {};
    for (const [k, v] of Object.entries(error.errors)) {
      fields[k] = v.message;
    }
    return res.status(422).json({ fields });
  }
  if (error.name === "CastError") {
    return res.status(400).json({ error: "ID invalid" });
  }
  next(error);
});

Mongoose throws ValidationError and CastError. Format them into 422 with specific fields. CastError happens with invalid ObjectIds. Translate DB errors into friendly responses.

Security and Performance


10 cards
CORS
const cors = require("cors");

// Allow everything (dev):
app.use(cors());

// Specific configuration:
app.use(cors({
  origin: "http://localhost:5173",
  methods: ["GET", "POST", "PUT", "DELETE"],
  credentials: true,
}));

cors allows requests from other domains. Without it, the browser blocks them (Same-Origin Policy). origin authorizes specific domains. credentials: true allows auth cookies/headers.

JWT authentication
const jwt = require("jsonwebtoken");

function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) return res.status(401).json({ error: "Token ausente" });

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    res.status(401).json({ error: "Token invalid" });
  }
}

jsonwebtoken verifies JWT tokens. The Authorization: Bearer token header is the standard. jwt.verify() validates the signature and expiration. req.user becomes available to the following handlers.

Trust proxy and HTTPS
// Behind Nginx/load balancer:
app.set("trust proxy", 1);

// Force HTTPS:
app.use((req, res, next) => {
  if (req.headers["x-forwarded-proto"] !== "https") {
    return res.redirect(301, `https://${req.hostname}${req.url}`);
  }
  next();
});

trust proxy makes Express read X-Forwarded-* headers (real IP, protocol). Without it, req.ip is the proxy. Force HTTPS in production with a 301 redirect. Essential behind load balancers.

Helmet
const helmet = require("helmet");

app.use(helmet());

// Or configure individually:
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "cdn.example.com"],
    },
  },
  crossOriginEmbedderPolicy: false,
}));

helmet() sets security headers: CSP, X-Frame-Options, HSTS, etc. It prevents XSS, clickjacking, MIME sniffing. Indispensable in production. Zero config already protects a lot.

Password hashing
const bcrypt = require("bcrypt");

// Registration:
const hash = await bcrypt.hash(password, 12);
await User.create({ email, password: hash });

// Login:
const user = await User.findOne({ email });
const valid = await bcrypt.compare(password, user.password);
if (!valid) return res.status(401).end();

bcrypt hashes with an automatic salt. hash(password, 12) — 12 rounds (more secure, slower). compare() checks without exposing the password. Never store passwords in plain text.

DDoS and payload limits
// Limit the body:
app.use(express.json({ limit: "100kb" }));

// Limit per IP (aggressive for login):
const loginLimit = rateLimit({
  windowMs: 60 * 1000,
  max: 5,
  message: { error: "Many tentativas" },
});
app.use("/login", loginLimit);

// Timeout:
app.use((req, res, next) => {
  req.setTimeout(30000);
  next();
});

Combine a limit on the body parser + rate limiting per endpoint. Login with an aggressive limit (5/min). A timeout prevents hanging connections. Layers of defense against abuse.

Rate limiting
const rateLimit = require("express-rate-limit");

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 min
  max: 100,
  message: { error: "Many orders, tente after" },
  standardHeaders: true,
});

app.use("/api/", limiter);

express-rate-limit limits requests per IP/window. max: 100 = 100 requests in 15 min. It returns 429 when exceeded. standardHeaders sends RateLimit-* headers. It prevents brute force.

Secure static files
app.use(express.static("public", {
  maxAge: "1d",
  etag: true,
  index: false,
  dotfiles: "ignore",
}));

// Immutable cache for hashed assets:
app.use("/assets", express.static("dist", {
  maxAge: "1y",
  immutable: true,
}));

express.static() serves files. maxAge sets caching. index: false prevents listing. dotfiles: "ignore" hides .env. immutable for assets with a fingerprint in the name.

Compression
const compression = require("compression");

app.use(compression());

// With a filter:
app.use(compression({
  filter: (req, res) => {
    if (req.headers["x-no-compress"]) return false;
    return compression.filter(req, res);
  },
  level: 6,
}));

compression gzips/brotlis responses. It reduces size by 60-80%. level (1-9) controls compression vs CPU. Apply it globally. In production with Nginx, it may be redundant.

Prevent injection
// NEVER:
db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);

// ALWAYS (parameterized):
db.query("SELECT * FROM users WHERE id = $1", [req.params.id]);

// Mongoose (safe by default):
User.findById(req.params.id);

// Escape HTML output:
const escapeHtml = require("escape-html");
res.send(`<p>${escapeHtml(name)}</p>`);

Always use parameterized queries ($1, ?) — never interpolation. Mongoose/ORMs protect by default. escape-html prevents XSS in the output. Validate param types.