Well-structured backends separate concerns: routers define URLs, controllers translate HTTP to application calls, services own data access, and middleware handles cross-cutting behavior (validation, 404, errors). Study these patterns to keep files small, testable, and aligned with REST conventions.

Server Architecture / Routes

Well-structured backends separate concerns: routers define URLs, controllers translate HTTP to application calls, services own data access, and middleware handles cross-cutting behavior (validation, 404, errors). Study these patterns to keep files small, testable, and aligned with REST conventions.

Select a function type from the sidebar, or browse topics below.

How to enable CORS — Intermediate — per router

Scope CORS to specific routers when only some paths need cross-origin access.
Learn: Router-scoped CORS limits cross-origin access to specific path prefixes—useful when only public API routes need browser access while admin routes stay same-origin only.

Supplementary examples

CORS on a single router

const cors = require("cors");
const publicRouter = require("./public/public.router");

app.use("/api/public", cors(), publicRouter);

Course example

const router = require("express").Router();
const controller = require("./cors-enabled.controller");
const methodNotAllowed = require("../errors/methodNotAllowed");
const cors = require("cors");
router.use(cors())
router
.route("/:corsId")
.get(controller.read)
.put(controller.update)
.delete(controller.delete)
.all(methodNotAllowed);
router
.route("/")
.get(cors(), controller.list)
.post(controller.create)
.all(methodNotAllowed);
module.exports = router;

Additional references & examples

How to create an Express router — Basic

Instantiate Router(), wire handlers, and export the router for mounting on the app.
Learn: An Express Router is a mini-app: it has its own middleware stack and routes, then mounts on the main app. `mergeParams: true` lets nested routers see parent params (e.g. `:userId` from a parent mount). Always export the router for `app.use`.

Supplementary examples

Router module skeleton

const router = require("express").Router({ mergeParams: true });

// routes here

module.exports = router;

Mount in app.js

const pastesRouter = require("./pastes/pastes.router");
app.use("/pastes", pastesRouter);

Course example

const router = require("express").Router({ mergeParams: true });

Additional references & examples

How to define routes on the app directly — Basic

Attach handlers with app.get/post before splitting logic into router modules.
Learn: Defining routes directly on `app` is fine for tiny projects or quick experiments. As endpoints multiply, inline handlers become hard to test and navigate—splitting into router modules keeps each file responsible for one resource area.

Supplementary examples

GET and POST on the app

app.get("/pastes", (req, res, next) => list(req, res, next));
app.post("/pastes", (req, res, next) => create(req, res, next));

Route parameters on the app

app.get("/users/:userId", (req, res) => {
  const { userId } = req.params;
  res.json({ userId });
});

Course example

// New middleware function to validate the request body
function bodyHasTextProperty(req, res, next) {
const { data: { text } = {} } = req.body;
if (text) {
return next(); // Call `next()` without an error message if the result exists
}
next("A 'text' property is required.");
}
let lastPasteId = pastes.reduce((maxId, paste) => Math.max(maxId, paste.id), 0);
app.post(
"/pastes",
bodyHasTextProperty, // Add validation middleware function
(req, res) => {
// Route handler no longer has validation code.
const { data: { name, syntax, exposure, expiration, text, user_id } = {} } = req.body;
const newPaste = {
id: ++lastPasteId, // Increment last id then assign as the current ID
name,
syntax,
exposure,
expiration,
text,
user_id,
};
pastes.push(newPaste);
res.status(201).json({ data: newPaste });
}
);

Additional references & examples

Query and route parameters — Lesson overview

Chegg Skills Lesson 5 (Node and Express): read three request inputs—path params, query string, and body—and use each in the right place in controllers and services.
Learn: Lesson 5 in the Node and Express unit introduces the three ways HTTP sends data into your API. Route parameters identify which resource (req.params). Query parameters optional filters (req.query). The body carries JSON on POST/PUT (req.body). Controllers read all three; routers only declare path parameter names in the URL pattern.

Course example

// Path parameter (router defines :userId)
// GET /users/42        ->  req.params.userId === "42"

// Query string (optional filters)
// GET /pastes?sort=desc&limit=5  ->  req.query.sort, req.query.limit

// JSON body (POST/PUT, needs express.json())
// POST /users  body: { "username": "ada" }  ->  req.body.username

Additional references & examples

How to use route parameters — Basic

Path segments like :userId and :pasteId are available on req.params. Define them in the router path; read them in the controller or service.
Learn: Route parameters are part of the path template. In /users/:userId/pastes/:pasteId, req.params.userId and req.params.pasteId are strings set by Express when the route matches. Use them to identify which resource to load or update. With nested routers, mergeParams: true lets a child router see :userId from the parent mount.

Supplementary examples

Nested mount + child param

// app.use("/users/:userId/pastes", pastesRouter);
// GET /users/3/pastes/9  ->  req.params.userId === "3", req.params.pasteId === "9"
async function read(req, res, next) {
  const { userId, pasteId } = req.params;
  // ...
}

Course example

// Router — path defines the parameter name
router.get("/:pasteId", controller.read);

// Controller — req.params matches :pasteId
async function read(req, res, next) {
  const { pasteId } = req.params;
  const paste = await service.read(pasteId);
  res.json({ data: paste });
}

Additional references & examples

How to define HTTP routes on a router — Basic

Map GET, POST, PUT, and DELETE handlers to paths using router methods or router.route().
Learn: REST maps actions to HTTP verbs: GET reads, POST creates, PUT/PATCH updates, DELETE removes. Use `router.route(path)` to chain verbs on the same path. Handlers should delegate to controllers, not contain SQL.

Supplementary examples

CRUD routes on one path

router
  .route("/:pasteId")
  .get(controller.read)
  .put(controller.update)
  .delete(controller.remove);

router.route("/").get(controller.list).post(controller.create);

Separate verb methods

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

Course example

const pastesRouter = require("../pastes/pastes.router");
router.use("/:userId/pastes", pastesRouter);

Additional references & examples

How to use query string parameters — Basic

Optional filters and flags after ? in the URL (e.g. ?sort=desc&limit=10) are on req.query. Use for sorting, pagination, and search—not for resource IDs.
Learn: Query parameters are optional and unordered. They are ideal for ?sort=name&limit=20 or ?active=true. Validate and parse types (Number, Boolean) in the controller before passing options to the service. Do not confuse req.query with req.params: /pastes/5 uses params for id 5; /pastes?sort=desc uses query for sort.

Supplementary examples

Pagination query params

const page = Math.max(1, Number(req.query.page) || 1);
const pageSize = Math.min(100, Number(req.query.pageSize) || 20);
const offset = (page - 1) * pageSize;

Pass query options to service

// GET /users/3/pastes?sort=desc
async function list(req, res, next) {
  const options = {
    userId: req.params.userId,
    sort: req.query.sort,
    limit: Number(req.query.limit) || 10,
  };
  const rows = await service.list(options);
  res.json({ data: rows });
}

Course example

// GET /pastes?sort=desc&limit=5
async function list(req, res, next) {
  const sort = req.query.sort || "asc";
  const limit = Number(req.query.limit) || 10;
  const pastes = await service.list({
    userId: req.params.userId,
    sort,
    limit,
  });
  res.json({ data: pastes });
}

Additional references & examples

How routers and controllers work together — Intermediate

The router owns URLs and HTTP verbs; the controller owns request/response logic. The router should only import the controller and connect paths—never contain SQL or heavy logic.
Learn: In the course architecture, the router answers WHERE and HOW (path + HTTP verb) while the controller answers WHAT HAPPENS for that request (read req.params, req.query, req.body, set status, send JSON). The router imports the controller object and passes function references—controller.list not controller.list()—so Express invokes them when a match occurs. Validation middleware, if any, is chained on the router before the controller. Services sit behind the controller for Knex/SQL. This split is what Organizing Express demonstrates with pastes and users modules.

Supplementary examples

Mount router on app (app.js)

const usersRouter = require("./users/users.router");
const pastesRouter = require("./pastes/pastes.router");

app.use("/users", usersRouter);
app.use("/users/:userId/pastes", pastesRouter);

Controller exports named handlers

// pastes.controller.js
module.exports = {
  list,
  create,
  read,
  update,
  remove,
};

Router chains validation then controller

const validateCreate = require("./pastes.validators");
router.post("/", validateCreate, controller.create);

methodNotAllowed for unsupported verbs

router
  .route("/:pasteId")
  .get(controller.read)
  .put(controller.update)
  .delete(controller.remove)
  .all(methodNotAllowed);

Course example

// pastes.router.js — wiring only (no business logic here)
const router = require("express").Router({ mergeParams: true });
const controller = require("./pastes.controller");
const methodNotAllowed = require("../errors/methodNotAllowed");

router
  .route("/")
  .get(controller.list)
  .post(controller.create)
  .all(methodNotAllowed);

router
  .route("/:pasteId")
  .get(controller.read)
  .put(controller.update)
  .delete(controller.remove)
  .all(methodNotAllowed);

module.exports = router;

Additional references & examples

How to mount routers with URL prefixes — Intermediate

Use app.use('/prefix', router) to group related endpoints under a base path.
Learn: Mounting groups related endpoints under a prefix (`/users/:userId/pastes`) keeps URLs readable and mirrors resource ownership. Order matters: register specific routes before catch-all middleware like notFound.

Supplementary examples

Nested resource mount

const usersRouter = require("./users/users.router");
const pastesRouter = require("./pastes/pastes.router");

app.use("/users", usersRouter);
app.use("/users/:userId/pastes", pastesRouter);

Course example

const usersRouter = require("./users/users.router");
const pastesRouter = require("./pastes/pastes.router");
app.use("/users", usersRouter);
app.use("/pastes", pastesRouter);

Additional references & examples

How to use a controller module — Intermediate

Keep route files thin by delegating request logic to controller functions.
Learn: A controller module exports functions the router references by name. Each function receives (req, res, next), calls the service layer, and shapes the HTTP response. The router never calls Knex; the controller never defines URL paths—that division is the core of the Organizing Express pattern.

Supplementary examples

Thin controller calling a service

async function list(req, res, next) {
  try {
    const pastes = await service.listByUser(req.params.userId);
    res.json({ data: pastes });
  } catch (err) {
    next(err);
  }
}

Set status for create

async function create(req, res, next) {
  try {
    const paste = await service.create(req.body);
    res.status(201).json({ data: paste });
  } catch (err) {
    next(err);
  }
}

Course example

// pastes.controller.js — HTTP in/out; calls service for data
const service = require("./pastes.service");

async function list(req, res, next) {
  try {
    const pastes = await service.list(req.params.userId);
    res.json({ data: pastes });
  } catch (err) {
    next(err);
  }
}

async function create(req, res, next) {
  try {
    const paste = await service.create(req.params.userId, req.body);
    res.status(201).json({ data: paste });
  } catch (err) {
    next(err);
  }
}

module.exports = { list, create };

Additional references & examples

How to use a service layer — Intermediate

Isolate database and business logic in services called from controllers or routers.
Learn: Services are the place for Knex queries, transactions, and business rules. Returning plain objects (or throwing errors) keeps controllers simple and makes services testable without spinning up HTTP.

Supplementary examples

Service function with Knex

const knex = require("../db/connection");

function listByUser(userId) {
  return knex("pastes").where({ user_id: userId }).orderBy("created_at", "desc");
}

module.exports = { listByUser };

Throw when resource missing

async function read(pasteId) {
  const row = await knex("pastes").where({ paste_id: pasteId }).first();
  if (!row) {
    const err = new Error("Paste not found");
    err.status = 404;
    throw err;
  }
  return row;
}

Course example

{
"data": [
{
"product_sku": "giUGNvnn8Q",
"product_title": "Reserved for the Dog Cushion",
"total_weight_in_lbs": "1100.00"
},
{
"product_sku": "ay6srUUaht",
"product_title": "Laudable Accomplishments of the Warrior: A Fate Forfeit",
"total_weight_in_lbs": "898.01"
},
...

Additional references & examples

How to validate request data — Intermediate

Run validation middleware before handlers so invalid body or params are rejected early.
Learn: Validate before controllers run so bad input never reaches the database. Return 400 with a clear message. Chain validators, then a small middleware that checks `validationResult` and stops the request.

Supplementary examples

express-validator chain

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

const validateCreate = [
  body("name").trim().isLength({ min: 1 }).withMessage("name is required"),
  body("tags").optional().isArray(),
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ error: errors.array() });
    }
    next();
  },
];

Course example

const User = require("./users.model")
async function list(req, res) {
const users = await User.find()
res.send(users);
}
async function userExists(req, res, next) {
const { userId } = req.params;
const foundUser = await User.findOne({"_id":userId});
if (foundUser) {
res.locals.user = foundUser;
return next();
}
next({
status: 404,
message: `User id not found: ${userId}`,
});
};
function read(req, res, next) {
res.json({ data: res.locals.user });
};
function bodyDataHas(propertyName) {
return function (req, res, next) {
const { data = {} } = req.body;
if (data[propertyName]) {
return next();
}
next({
status: 400,
message: `Must include a ${propertyName}`
});
};
}
async function create(req, res) {
const { data: { username, email} = {} } = req.body;
const newUser = new User({
username: username,
email: email,
})
await newUser.save()
res.status(201).json({ data: newUser });
}
async function update(req, res) {
const user = res.locals.user;
const { data: { username, email } = {} } = req.body;
user.username = username;
user.email = email;
await user.save()
res.json({ data: user });
}
async function destroy(req, res) {
const { userId } = req.params;
await User.deleteOne({ _id: userId })
res.sendStatus(204);
}
module.exports = {
list,
read: [userExists, read],
create: [
bodyDataHas("username"),
bodyDataHas("email"),
create
],
update: [
userExists,
bodyDataHas("username"),
bodyDataHas("email"),
update
],
delete: [userExists, destroy],
userExists,
};

Additional references & examples

How to handle unknown routes — Basic

Register a notFound middleware after all routes to return a consistent 404 response.
Learn: If no route matched, Express falls through to the next middleware. Register notFound after all routers so clients get a consistent JSON 404 instead of a generic HTML page.

Supplementary examples

notFound middleware

function notFound(req, res, next) {
  res.status(404).json({ error: `Not found: ${req.method} ${req.originalUrl}` });
}

// in app.js, after all routes:
app.use(notFound);

Course example

const express = require("express");
const cors = require("cors");
const app = express();
const corsEnabledRouter = require("./cors-enabled/cors-enabled.router");
const corsNotEnabledRouter = require("./cors-not-enabled/cors-not-enabled.router");
const notFound = require("./errors/notFound");
const errorHandler = require("./errors/errorHandler");
app.use(cors());
app.use(express.json());
app.use("/cors-enabled", corsEnabledRouter);
app.use("/cors-not-enabled", corsNotEnabledRouter);
app.use(notFound);
app.use(errorHandler);
module.exports = app;

Additional references & examples

How to handle errors globally — Intermediate

Use a centralized errorHandler middleware to format errors and set status codes.
Learn: Express recognizes error-handling middleware by its four arguments `(err, req, res, next)`. Log the error, map known status codes, and avoid leaking stack traces in production.

Supplementary examples

Central error handler

function errorHandler(err, req, res, next) {
  const status = err.status || err.statusCode || 500;
  req.log?.error(err);
  res.status(status).json({
    error: err.message || "Internal Server Error",
  });
}

app.use(errorHandler); // after notFound

Course example

const checkForAbbreviationLength = (req, res, next) => {
const abbreviation = req.params.abbreviation;
if (abbreviation.length !== 2) {
next("State abbreviation is invalid.");
} else {
next();
}
};

Additional references & examples