Chapter 5: Request Handlers (Controllers)
Welcome back! In our last chapter, Access Control (Authentication & Authorization), we saw how our application uses the Express.js web server (API Web Server (Express.js)) and its middleware pipeline to receive requests, identify who is making the request (Authentication), and check if they are allowed to perform the requested action (Authorization).
So, a request comes in, Express receives it, middleware checks its validity and the user's permissions. What happens after these checks pass? The request has been verified and is ready to be processed for its actual purpose – like getting a list of events or creating a new department.
This is job of the Request Handlers, often referred to as Controllers.
The Department Heads
Let's return to our office building analogy.
- Express.js is the Receptionist who receives all incoming visitors (requests).
- Middleware (including Authentication & Authorization) is the Security Guard who checks IDs and permissions at the entrance.
- Models are the Expert Workers (like the "Event Expert" or "User Expert") who know how to deal with specific types of data and interact with the archive (Prisma).
Who connects the approved visitors (requests) to the correct Expert Worker (Model) and makes sure the task gets done? That's the Department Head or Manager.
In our events-api
, the Request Handlers (Controllers) are these managers. They are the specific functions that Express's router (API Web Server (Express.js)) directs the request to once it knows what the request is asking for and that the user is allowed to ask for it.
Their main job is to:
- Receive the incoming request (which has already passed authentication and authorization).
- Understand the specific action needed (e.g., "find event by ID", "create new department").
- Delegate the core task to the appropriate Model (Data Logic (Models)), providing any necessary information from the request (like user ID, event data from the body, or a record ID from the URL).
- Format the result received back from the Model (or handle errors).
- Send the final response back to the client using the
res
object.
They don't contain the detailed business logic (that's the Models') nor do they handle infrastructural tasks like parsing JSON or checking auth (that's the middleware's). They are the conductors, orchestrating the flow for a specific request.
Our Use Case: Handling the "Create Event" Request
Let's stick with our example from the previous chapter: Creating a new event.
We know that a POST
request to /api/v1/events
arrives, passes through middleware (logging, parsing, authentication, authorization), and is then matched by the Express router to a specific function. That function is the Controller function responsible for creating events.
Where do these functions live? They are organized in files within the src/controllers
directory, typically one file per major data type (e.g., src/controllers/events.ts
, src/controllers/users.ts
, src/controllers/departments.ts
).
In src/controllers/events.ts
, you'll find a function named create
. This is the function our router (API Web Server (Express.js)) points to for incoming POST /events
requests.
Looking at an Example: src\controllers\events.ts
create
function
Let's simplify the actual create
function from src/controllers/events.ts
to see its core structure:
// Simplified src\controllers\events.ts - create function
import { RequestHandler } from 'express'; // Import Express type
// ... other imports needed by the controller ...
import Events from '../models/event'; // Import the Events Model
// This function handles POST requests to /api/v1/events
export const create: RequestHandler = async (req, res, next) => {
// 1. Get necessary data from the request
// The request body (req.body) contains the data for the new event.
// Middleware (like express.json) has already parsed this into a JS object.
const { start, end } = req.body;
// We know the user is authenticated and authorized because middleware ran before this!
// The authenticated user is available on req.user
const user = req.user!; // The '!' tells TypeScript user is definitely here
try {
// 2. Delegate the core task to the appropriate Model
// Call the createModel function on the Events Model, passing the user and event data.
const event = await Events.createModel(user, start, end);
// 3. & 4. Format and Send the response
// Set the HTTP status code to 201 (Created)
res.status(201);
// Send the created event object back as a JSON response
res.json(event);
// The actual code also adds notifications here for Socket.IO, which we'll cover later.
// res.notifications = [...];
} catch (e) {
// If any error occurs during the process (e.g., database error, Model validation error)
// Pass the error to the next middleware (our error handling middleware)
next(e);
}
};
Explanation:
export const create: RequestHandler = async (req, res, next) => { ... }
: This defines an asynchronous function namedcreate
and types it as an ExpressRequestHandler
.RequestHandler
is a type definition provided by Express to clarify that this function expects three parameters:req
(the Request object),res
(the Response object), andnext
(a function to call to pass control to the next middleware or error handler).const { start, end } = req.body;
: It accesses thebody
of the incoming request (req.body
). Express middleware likeexpress.json()
(which we saw in API Web Server (Express.js)) automatically parses the JSON body of the request and makes it available as a JavaScript object here. We're extractingstart
andend
date/time for the new event from this body.const user = req.user!;
: It accesses theuser
object previously attached to the request by the Authentication & Authorization middleware (specifically Passport). The Controller needs to know who is creating the event, as this information is often required by the Model (e.g., to set the event's author).const event = await Events.createModel(user, start, end);
: This is the core delegation step. The Controller doesn't contain the complex logic for inserting an event into the database or performing related actions. Instead, it calls thecreateModel
function on theEvents
Model (Data Logic (Models)). It passes the authenticateduser
and the necessary event data (start
,end
) to the Model. Theawait
keyword is used because creating data in the database takes time, and the Model function returns a Promise.res.status(201).json(event);
: If the Model successfully creates the event, the Controller receives the resulting event object back. It then uses theres
(Response) object to construct and send the final HTTP response:res.status(201)
sets the HTTP status code to 201, which means "Created". This is the standard success code for resource creation.res.json(event)
formats theevent
object into a JSON string and sends it back to the client.
catch (e) { next(e); }
: This is standard Express error handling. If anything goes wrong within thetry
block (e.g., the Model throws an error because the data was invalid, or a database connection fails), thecatch
block catches the error and callsnext(e)
. This passes the error down the middleware pipeline to Express's built-in error handler, or our custom error handling middleware, which will format an appropriate error response (like a 400 or 500 status code).
This simplified example demonstrates that the Controller for create
acts as a thin layer that translates the incoming HTTP request (get data from body, identify user) into a clear instruction for the relevant Model ("create an event for this user with this start/end time").
What Happens Inside Other Controllers?
Controllers for other API endpoints follow a similar pattern:
GET /api/v1/events
(all): Insrc/controllers/events.ts
, theall
function likely callsEvents.published()
orEvents.all(req.user)
. It then sends the result usingres.json()
.GET /api/v1/events/:id
(find): Thefind
function takes:id
fromreq.params.id
, callsEvents.findModel(req.user, req.params.id)
, and sends the found event viares.json()
.PUT /api/v1/departments/:id
(update): Theupdate
function insrc/controllers/departments.ts
gets the ID fromreq.params.id
, the update data fromreq.body.data
, callsDepartments.updateModel(req.user!, req.params.id, req.body.data)
, and sends the updated department viares.json()
.DELETE /api/v1/users/:id
(destroy): Thedestroy
function insrc/controllers/users.ts
gets the ID fromreq.params.id
, callsUsers.destroy(req.user!, req.params.id)
, and typically sends a204 No Content
status back if successful (res.status(204).send()
).
You can look at the other controller files (src/controllers/*.ts
) linked in the code snippets section to see many more examples of this pattern. Each exported function in these files is a Request Handler (Controller) for a specific route registered in src/routes/router.ts
.
The Flow with Request Handlers
Let's revisit the sequence diagram from the previous chapter, focusing on the Controller's place in the flow for our "Create Event" example (POST /api/v1/events
):
This diagram clearly shows the Controller receiving the request after middleware and routing, then interacting with the Model to perform the actual data operation, and finally using the Server/Express to send the response back.
Conclusion
Request Handlers or Controllers are the layer in our Express application responsible for the final step of processing an incoming request after it has passed through authentication and authorization middleware. They are the specific functions mapped by the router to different API endpoints (URL path + HTTP method).
Their key role is to extract necessary information from the request (req
), delegate the core business logic to the appropriate Model (Data Logic (Models)), handle the Model's response or any errors, and format the final HTTP response (res
) to be sent back to the client. They act as the coordination point between the web server layer and the application's data logic layer.
We now have a solid understanding of how requests come into the API, get secured, and are then directed to specific code that uses our data Models and Prisma to interact with the database. But what about data that doesn't come directly from client requests? Our application also needs to get data from external systems automatically.
Next Chapter: External Data Sync
Generated by AI Codebase Knowledge Builder