intermediate28 min

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.

Loading editor…

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.

Loading editor…

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.

Loading editor…

Stuck? Get a hint.

Guided local lab

Set Up a Real Express Server with Modular Routes

Runs on your computer
This lab runs on your own computer, in your own terminal and editor — not in your browser. VisaSparkSchools does not execute, run, or verify these commands for you. Follow the verification steps yourself to confirm your result.

Create 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

  1. Create a new folder `learning-api` and run `npm init -y` inside it.
  2. Run `npm install express`.
  3. Add `"type": "module"` to the generated package.json so `import`/`export` syntax works.
  4. 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.json

Starter 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.js
  • Test a GET request (in a second terminal)

    curl http://localhost:3001/courses
  • Test 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/1

    Expected: A JSON object for the HTML & CSS Fundamentals course

  • curl -i http://localhost:3001/courses/999

    Expected: 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 startupConfirm "type": "module" is present in package.json — without it, Node expects CommonJS require() syntax instead of import.
  • req.body is undefined inside the POST handlerConfirm `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 404Make 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

Knowledge check

1. How does Express decide which registered route handles an incoming request?
2. Why organize routes into separate Router modules instead of one large file?
3. Why is this lesson's real server-setup work in a guided local lab rather than a browser Run button?

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.