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.
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.
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`.
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.
// 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 });
}
);
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
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.
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.
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.
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.
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.
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.
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.
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.
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);
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