Module 1 · Understanding the layers

1.3 Reading backend logic: routes, controllers, and the path of a request

Lesson 1.3 · 14 min read

In the last post you learned to read the frontend, the code that runs on the user's device and paints the screen. Now we cross to the other side of the journey, to the server, and learn to read what happens there. This is the half of software that most designers never see and quietly assume is beyond them. It isn't. Backend code is arguably more readable than frontend code, because it's more linear: a request comes in, a clear sequence of steps runs, a response goes out. Once you can follow that path, the server stops being a black box and becomes something you can reason about, debug, and change.

We're going to trace a single request all the way through a real backend, step by step, in code you can actually read. By the end, you'll be able to open a server file, find where a request lands, follow what it does, see where it touches the database, and understand what it sends back. That's the skill that lets you own the whole product, not just its surface.


What the backend is actually for

First, let's be clear about why the backend exists at all, because it sharpens everything that follows.

The frontend can't be trusted with the important things, and it can't hold the real data. Two hard reasons. First, the frontend runs on the user's own device, which means the user can see it, poke at it, and tamper with it. Anything secret (real business logic, the keys to the database, the rules about who's allowed to do what) cannot live there, because the user could simply look. Second, the frontend is just one visitor's view. The actual shared truth of the product, every user's account, every order, every message, has to live somewhere central that everyone connects to, not scattered across millions of individual devices.

That central, trusted place is the backend. It holds the real data (through the database), it enforces the real rules (who can do what), and it does the work that has to be done somewhere safe and shared. Think of the frontend as the customer-facing counter of a bank and the backend as the vault and the back office. The counter is where you interact, but no one keeps the money or the master records at the counter. The counter passes your request to the back, where the real, protected work happens, and brings back the answer. The backend is the back office of every product you use.

So when a frontend component runs fetch("/api/products"), as we saw last post, it's sending a request across to this back office. Now we go behind the counter and watch what happens when that request arrives.


The four stops every request makes

Almost every backend request, in almost every product, moves through the same four stops in order. Learn these four and you have the skeleton of all backend code. Everything else is detail hanging on this frame.

A request arrives. First it hits the route, which decides where this request should go based on its address. Then it reaches the controller (also called a handler), the code that actually does the work for this specific request. The controller usually needs data, so it talks to the database, asking for or changing information. Finally, the controller assembles a response and sends it back across to the frontend. Route, controller, database, response. That's the path. Let's walk each stop with real code.

Interactive: request path backend

A hands-on visual for this idea is in the works. The text around it carries everything you need for now.


Stop one: the route, a switchboard for requests

When a request arrives at the backend, the very first question is: what is this request even asking for? A request for the list of products and a request to create a new order are completely different jobs, and they need to go to completely different code. The route is what makes that decision. It's a switchboard, matching each incoming request to the right piece of code based on the request's address and type.

Here's what routes look like in code. This is a common style, and it's remarkably readable:

app.get("/api/products", getProducts);
app.post("/api/orders", createOrder);
app.get("/api/users/:id", getUser);

Read each line as a rule. app.get("/api/products", getProducts) says: "when a GET request arrives asking for the address /api/products, hand it to the code called getProducts." The next line says: "when a POST request arrives at /api/orders, hand it to createOrder." Each route connects an address to a piece of code that handles it.

Two things worth noticing, because they carry real meaning. First, get and post. These are the request types from way back in the journey. GET means "I want to retrieve something" (get me the products). POST means "I want to create or submit something" (here's a new order, please create it). There are a few others (PUT and PATCH for updating, DELETE for removing), but GET and POST are the two you'll see most, and the words tell you the intent: GET reads, POST writes. Just seeing which one a route uses tells you whether it's fetching data or changing it.

Second, that :id in /api/users/:id. The colon marks a placeholder. This one route handles requests for any user: /api/users/42, /api/users/99, whoever. The actual id from the address gets captured and passed to the handler, so getUser knows which specific user was asked for. This is how one piece of code serves millions of users: the route captures which one, and hands that detail down. When you see a colon in a route address, read it as "a slot that gets filled with a real value from each request."

So a routes file, which every backend has, is genuinely just a list of these rules, a directory of every address the backend responds to and which code handles each. It's often the best place to start reading an unfamiliar backend, because it's a table of contents for the entire server. Want to know everything this backend can do? Read its routes. Each line is one capability.


Stop two: the controller, where the work happens

The route pointed at a piece of code. That code is the controller (or handler), and it's where the actual work for this request gets done. If the route is the switchboard, the controller is the person who picks up and handles your specific request from start to finish.

Let's read a real one, the getProducts that our route pointed to. Take it slowly; you'll find you already understand most of it.

async function getProducts(request, response) {
  const products = await db.query("SELECT * FROM products");
  response.json(products);
}

Start with the shape. It's a function named getProducts, and from the last post you know a function is a named set of instructions. It takes two inputs, request and response, and these two are the heart of all backend work. The request is everything about the incoming ask: who sent it, what they want, any data they included. The response is the tool for sending an answer back. Nearly every controller you'll ever read takes these two: the question coming in, and the means to answer it.

Now the body, two lines. The first: const products = await db.query("SELECT * FROM products"). Read the readable part first. db.query(...) means "ask the database something." The thing in quotes, SELECT * FROM products, is that something, a database question meaning "get all the products." (That's SQL, the language for talking to databases. We'll give it a whole module later. For now, SELECT * FROM products reads almost like English: select everything from products.) The result gets stored in a box named products. So this line says: ask the database for all products, and hold the answer in products.

The word await in front deserves a note, because it appears constantly in backend code and it's genuinely useful to understand. Talking to a database takes a moment, it's a real trip to another system, remember the latency posts. await means "wait here for this to finish before moving on." Without it, the code would rush ahead before the products came back and try to use data that isn't there yet. await says "pause on this line until the database answers, then continue." (And async at the top of the function is just the required signal that this function contains an await inside it. Wherever you see await, the function around it will be marked async. They travel together.) So when you see await, read it as "this line involves waiting for something, and the code correctly pauses for it." It marks the slow steps, the trips to other systems, which is exactly what you want to be able to spot.

The second line: response.json(products). Remember response is the tool for answering. .json(products) means "send these products back to the frontend as JSON," the structured data format from the journey posts. So this line ships the answer across to the waiting frontend component, which, as you now know, will save it into state and re-render the screen with it.

Read the whole controller as one plain sentence: "when a request for products comes in, ask the database for all the products, then send them back as JSON." That's it. That's a real backend endpoint, fully understood. The frontend's fetch("/api/products") from last post now has its other half: this is the code that catches that fetch and answers it. You've now seen both ends of a single request, the ask and the answer, in real code.


Stop three: talking to the database, reading and writing

The getProducts controller only read from the database. But plenty of requests need to change things, create an order, update a profile, delete a comment, and reading how those work rounds out the picture. Let's look at the createOrder controller that our POST route pointed to, because writing data has one extra move worth seeing.

async function createOrder(request, response) {
  const newOrder = request.body;
  const saved = await db.insert("orders", newOrder);
  response.json(saved);
}

The shape is familiar: a function taking request and response. But look at the first line: const newOrder = request.body. This introduces something important. When a request comes in to create something, it carries the data to be created along with it, and that data lives in request.body. Remember from the journey that a request can carry details? The body is where those details ride. For a "create an order" request, the body holds the order itself: the items, the quantities, the address. So this line pulls the incoming order data out of the request and holds it in newOrder. Whenever you see request.body, read it as "the data the frontend sent along with this request," and it's almost always on POST or PUT requests, the ones that write.

The second line: await db.insert("orders", newOrder). You can read this now. db.insert(...) means "add something new to the database." It's adding newOrder into the collection of orders. And await is there because, again, it's a trip to the database that takes a moment, so the code correctly pauses for it. The result, the saved order, gets held in saved. The third line sends that saved order back as the response, confirming to the frontend that it worked.

So now you can see the two fundamental directions of database work. Reading, pulling existing data out to send back (the products example). Writing, taking data that came in and saving or changing it in the database (the order example). Almost everything a backend does is some combination of these: read something and return it, or take something in and write it down. When you read any controller, you can now ask a clarifying question that cuts right to its purpose: is this reading from the database, writing to it, or both? That single question tells you most of what the endpoint is for.


Stop four: the response, and the round trip closes

You've already seen the response in both controllers: response.json(...). It's the final act of every request, the backend sending its answer back across the wire to the frontend that asked.

But responses carry one more piece of meaning worth knowing, because it's central to how the frontend and backend actually communicate about success and failure: the status code. Every response comes with a number that summarizes how things went. You've seen these without knowing it. 200 means "OK, success." 404 means "not found," the thing you asked for doesn't exist. 401 means "you're not authorized," you need to log in or you're not allowed. 500 means "the server hit an error," something broke on the backend. These codes are how the backend tells the frontend, in a single number, what happened, so the frontend knows whether to show the data, show a login prompt, or show an error message.

A more complete controller often sets these deliberately:

async function getUser(request, response) {
  const user = await db.findUser(request.params.id);
  if (!user) {
    response.status(404).json({ error: "User not found" });
  } else {
    response.status(200).json(user);
  }
}

You can read all of this now, and notice how much you've gained. It fetches a user by the id from the address (request.params.id, that placeholder slot from the route, now filled). Then it makes a decision, the if from the conditional patterns you learned: if no user was found, respond with a 404 and an error message; otherwise, respond with 200 and the user. This is a real, well-behaved endpoint, and you just read it completely, including how it handles the case where something's missing. That handling of the unhappy path, what happens when the thing isn't there or something goes wrong, is a large part of what separates toy backends from real ones, and now you can see it in the code.


Reading the full round trip, both sides at once

Let's step back and assemble the entire journey, because you can now read all of it, frontend and backend together, which almost no designer can do.

A user opens a screen. The frontend component, on load, runs fetch("/api/products"), sending a GET request across to the backend. The request travels the network (with all the latency from the journey posts) and arrives at the backend. The route matches /api/products and hands the request to the getProducts controller. The controller runs db.query("SELECT * FROM products"), making a trip to the database, and awaits the result. The database returns all the products. The controller sends them back with response.json(products) and a 200 status. The response travels back across the network to the frontend. The waiting component receives the data, calls setProducts(data), which changes its state, which triggers React to re-render, and the screen fills with a card for each product.

Read that paragraph again and notice something: there is not a single step in it you don't understand. From the tap, through the network, into the route, through the controller, to the database and back, across the response, into state, onto the screen. That is a complete round trip through a real full-stack application, and you can trace every stop. This is what "full stack" actually means, the ability to follow a request all the way down and all the way back, and you now have it. Not vaguely. Concretely, in code.


Why this makes you a real builder

Here's what this reading skill unlocks, especially working with AI. When you ask an AI to build a feature, a real feature and not just a screen, it produces both halves: frontend components and backend routes and controllers. Most people can, at best, squint at the frontend. You can now read the whole thing, and that changes what kind of builder you are.

You can verify the backend does what you intended: that the right route exists, that the controller reads or writes the right data, that it handles the missing-thing case, that it sends a sensible response. When a feature breaks, you can reason about which side is at fault, which is often the hardest and most valuable debugging question there is. Is the frontend not showing the data because the fetch failed, or because the backend sent back nothing, or because the database query was wrong? You can now trace the request across both sides and find where it actually went wrong, instead of guessing. And you can direct changes precisely on either side: "the controller should also check that the user is allowed to see this order before returning it," "add a route for updating a product," "return a 404 when the product doesn't exist instead of an empty response." That's the language of someone shaping a real system, not decorating its surface.

This is the moment you stop being a designer who can also make screens, and start being a design engineer who can own a feature end to end, its look, its behavior, its data, and its logic, across the entire stack. The reading skill from the last two posts is the foundation of that, and now it spans both sides of the divide.


Do this before the next post

Ask any AI to "write a simple backend with Express that has a route to get all products and a route to create a new product, with the controllers included." You'll get back something close to what we read in this post. Now trace it, the same way we did.

Find the routes first: what addresses does this backend respond to, and which are GET (reading) versus POST (writing)? Then follow each route to its controller and read what it does: where does it talk to the database, is it reading or writing, and what does it send back? Point at the request, the response, the database call, and the await on each slow step. If you can look at a small backend and narrate what happens when a request hits each route, from arrival to answer, you can read backend code. Combined with the last post, you can now read a full application, both halves of every request. That is a genuinely rare and powerful thing, and it's the ground the rest of this series builds on.