Express Application Structure and Routing
How Express matches an incoming request to the right handler, and setting up a real, modularly-routed Express server on your own machine.
What you'll learn
- Explain how Express matches an incoming request's method and path to a registered route
- Organize routes into modular, feature-based files rather than one large file
- Set up and run a real local Express server with modular routing
Prerequisites
Explanation
Express matches an incoming request to a handler by checking its registered routes, in the order they were registered, for the first one whose method and path pattern both match. app.get("/courses/:id", handler) matches a GET request whose path looks like /courses/anything, capturing anything as req.params.id. Route matching is not "smartest match wins" — it's "first registered match wins," which is exactly why route order matters: a more specific route (/courses/featured) registered after a more general one with a parameter (/courses/:id) will never actually be reached, because /courses/:id matches /courses/featured first, treating "featured" as an id.
A real application's routes should not all live in one growing file. Express's Router lets you group related routes into their own module — a courses.routes.js file exporting a Router with all course-related endpoints, mounted onto the main app with app.use("/courses", coursesRouter). This is the same "organize by feature" principle from earlier in this curriculum, applied to a backend: a project with users, courses, and enrollments as separate concerns should have separate route modules for each, not one file where all three are tangled together.
This lesson's guided local lab is where you set up a genuinely running Express server for the first time — nothing about routing, request matching, or server startup can be simulated honestly in this browser sandbox; it needs a real Node process listening on a real local port.
Example
A simplified route-matching function -- the same core algorithm (method + path pattern matching, first-match-wins, order-dependent) that Express itself implements.
function matchRoute(routes, method, path) {
for (const route of routes) {
if (route.method !== method) continue;
const pattern = route.path.replace(/:[^/]+/g, "([^/]+)");
const match = path.match(new RegExp("^" + pattern + "$"));
if (match) return { handler: route.handler, params: match.slice(1) };
}
return null;
}
const routes = [
{ method: "GET", path: "/courses/featured", handler: "getFeatured" },
{ method: "GET", path: "/courses/:id", handler: "getById" },
];
console.log(matchRoute(routes, "GET", "/courses/featured")); // matches getFeatured -- registered first
console.log(matchRoute(routes, "GET", "/courses/42")); // matches getById, params: ["42"]Try it yourself
Swap the order of the two routes in the array and re-run -- notice /courses/featured no longer reaches its intended handler.
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Guided exercise
Guided exercise
Using matchRoute already defined, register routes in the CORRECT order so that both '/users/me' (a specific route) and '/users/:id' (a parameterized route) both work correctly, then confirm by matching '/users/me' and '/users/42'.
Checks: the specific route wins for /users/me · the parameterized route still works for a real id
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Independent exercise
Independent exercise
Write groupRoutesByFeature(routes) where routes is an array of { path, handler }. Group them by the first path segment (e.g. '/courses/featured' and '/courses/:id' both belong to 'courses'), returning an object mapping each feature name to its array of routes.
Checks: correctly groups multiple routes under one feature · correctly groups a single route under its own feature
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Guided local lab
Set Up a Real Express Server with Modular Routes
Runs on your computerCreate a real Express server on your own machine for a learning-progress API, with courses and enrollments organized into separate route modules, mounted onto the main app.
Required tools
- Node.js (20.x or 22.x LTS)
- npm (10.x (bundled with Node.js))
Setup
- Create a new folder `learning-api` and run `npm init -y` inside it.
- Run `npm install express`.
- Add `"type": "module"` to the generated package.json so `import`/`export` syntax works.
- Create the files below in the structure shown, then run `node src/server.js`.
Project structure
learning-api/
src/
server.js
routes/
courses.routes.js
enrollments.routes.js
package.jsonStarter files
src/routes/courses.routes.js
import { Router } from "express";
const router = Router();
const COURSES = [
{ id: 1, title: "HTML & CSS Fundamentals" },
{ id: 2, title: "JavaScript Fundamentals" },
];
router.get("/", (req, res) => {
res.json(COURSES);
});
router.get("/:id", (req, res) => {
// TODO: find the course by id (req.params.id) and respond with it,
// or respond with a 404 if no course matches.
});
export default router;src/routes/enrollments.routes.js
import { Router } from "express";
const router = Router();
const ENROLLMENTS = [];
router.get("/", (req, res) => {
res.json(ENROLLMENTS);
});
router.post("/", (req, res) => {
// TODO: push a new enrollment (req.body) onto ENROLLMENTS and
// respond with 201 and the created enrollment.
});
export default router;src/server.js
import express from "express";
import coursesRouter from "./routes/courses.routes.js";
import enrollmentsRouter from "./routes/enrollments.routes.js";
const app = express();
app.use(express.json());
app.use("/courses", coursesRouter);
app.use("/enrollments", enrollmentsRouter);
const PORT = 3001;
app.listen(PORT, () => {
console.log("Learning API listening on port " + PORT);
});Requirements
- Course and enrollment routes live in their own separate router modules, not all in server.js
- GET /courses/:id returns the matching course, or a 404 status if no course has that id
- POST /enrollments accepts a JSON body and adds a new enrollment, responding with 201 and the created record
- The server starts without errors and logs the port it's listening on
Commands to run
Start the server
node src/server.jsTest a GET request (in a second terminal)
curl http://localhost:3001/coursesTest a POST request (in a second terminal)
curl -X POST http://localhost:3001/enrollments -H "Content-Type: application/json" -d "{\"courseId\":1}"
Expected behavior
GET /courses returns the two seed courses as JSON. GET /courses/1 returns that specific course; GET /courses/999 returns a 404. POST /enrollments with a JSON body creates and returns a new enrollment with status 201.
Verify it yourself
curl http://localhost:3001/courses/1Expected: A JSON object for the HTML & CSS Fundamentals course
curl -i http://localhost:3001/courses/999Expected: HTTP status 404, since no course has that id
curl -i -X POST http://localhost:3001/enrollments -H "Content-Type: application/json" -d "{\"courseId\":1}"Expected: HTTP status 201 with the newly created enrollment echoed back in the response body
Troubleshooting
- "Cannot use import statement outside a module" error on startup — Confirm "type": "module" is present in package.json — without it, Node expects CommonJS require() syntax instead of import.
- req.body is undefined inside the POST handler — Confirm `app.use(express.json())` is registered in server.js before the routes that need to read a JSON body — without it, Express never parses the incoming body.
- GET /courses/999 returns a generic error page instead of a clean 404 — Make sure the route handler explicitly checks whether a matching course was found and calls res.status(404).json(...) rather than letting an undefined value flow into res.json().
Stuck? Get a hint.
Extension challenge
Add a GET /enrollments/:id route, and a DELETE /enrollments/:id route that removes an enrollment, responding 404 if the id doesn't exist.
When you've verified this locally, use the "Mark lesson complete" button below to record your progress.
Common mistakes
- Registering a general parameterized route (`/courses/:id`) before a more specific one (`/courses/featured`), so the specific route is never actually reached.
- Putting every route directly on the main `app` object in one growing file instead of organizing them into feature-based Router modules.
- Forgetting `express.json()` middleware, then being confused why `req.body` is undefined in a POST handler.
Knowledge check
Takeaway
Express matches routes in registration order, first-match-wins — specific routes must be registered before general parameterized ones — and real routes belong in feature-organized Router modules, set up and run on your own machine.
Summary
This lesson covered Express's route-matching algorithm and route-ordering pitfalls through browser exercises, then set up a real, modularly-routed Express server with working GET and POST endpoints via the guided local lab.
References
Your notes
Notes save automatically.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.