What is the difference between Middleware and Controllers in Node REST APIs?

When you're writing code dealing with the request lifecycle, what is the difference between middleware and controllers? What code goes in each? They both touch the "request object" and do things with it, so it seems like there might be overlap or controllers might be redundant to have.

But they are distinctly different, and understanding the differences in the use cases for each logic type is important in structuring maintainable and scalable applications. Let's examine the distinctions between the two and what code goes where, so the next time you are working on an API you can build it sustainably.

Note: this post focuses on middleware and controllers from a NodeJS perspective but is applicable to other languages and frameworks.

Middleware vs. Controllers

What's immediately confusing is that a controller can be considered a type of middleware. (And to add to the confusion, "controllers" are sometimes referred to as "route handlers".) But middleware - compared to controllers - handles HTTP-specific concerns as well as concerns that are common to every (or most) request. "HTTP concerns" are things like handling CORS, parsing the body, cookies, etc. "Common concerns" are things like request validation, sessions, request logging using something like morgan, and authentication and authorization. Middleware is basically agnostic of application logic.

All of the above you don't want in your controllers. Why? Because controllers are responsibile for routing a given request where it needs to go for "processing". They don't directly deal with application logic, but send the request to code that does. Controllers should be fairly "thin" and not do a whole lot. So, a "book order" controller would take the request object after it's already been "pre-processed" by middleware and "successfully passed" it, pull out what data it needs from either the query string or body and send it to the service layer/domain logic layer to execute the business logic.

Middleware functions are typically (although not always) passed through by all HTTP requests and do not fulfill the request, unless it's a case of returning an error response early. It's the controller that ultimately fulfills the request with a successful response. They are usually are specific to a single endpoint (like /items, /orders, etc).

Example code

Controller code will look something like this:

const express = require('express')
const router = express.Router()

// Controller
const createOrder = async (req, res, next) => {
  // grab what we need from the request...
  const {customerId, orderTotal, orderItems, paymentDetails} = req.body
  
  try {
    // ...then route it to the appropriate business-logic-processing functions...
    const customerData = await getCustomerData(customerId)
    await processOrder(orderTotal, orderItems, paymentDetails, customerData)
    await sendConfirmationEmailToCustomer(customerId, orderItems)

    // ...then fulfill the request with the response object
    res.sendStatus(201)

    return
  } catch (err) {
    res.sendStatus(500)

    return
  }
}

// Route definition
router.post('/order', createOrder)

module.exports = router

Above you will notice that the controller grabs what we need from the request, routes it to the appropriate business-logic-processing function(s), and finally fulfills the request via the response (res) object.

Then the chain of middleware will look similar to this, usually called either app.js or server.js:

const express = require('express')
const app = express()
// middleware library imports
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const passport = require('passport') // for auth

const routes = require('./routes') // this is a .js file that contains all your route definitions mapped to their respective controllers

// here is the middleware being chained together
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(cookieParser)
app.use(passport.initialize())

// routes defined last in the chain of middleware
app.use('/api', routes)

In the above, we chain the middleware together, using app.use(), before we have the routes (which map to controllers) at the end of the chain, so that the request has to pass through the middleware in order before even hitting the controllers.

Note: if I need to build custom middleware I will generally add a directory called middleware/ to be at the same level as the controllers/ directory, and put my custom middleware functions there.

Summary

While it may seem confusing at first, the difference between these two "layers" / logic types are generally fairly clear, so it should make sense now as to what code goes where. Occasionally, you may run into a scenario where it's less black and white, but then you just have to decide what makes sense for where to put the code based on you specific application requirements.

Why does this matter? If it's not obvious, this separation of concerns is neccessary for developing a maintainable codebase and API. You will save yourself a lot of painful refactoring in the future if you can achieve this separation, because as your code grows and you add more developers to the team things can start to get out of hand quickly.

Found this post helpful? One of the most frustrating things with Node is how there aren't many "official" patterns - defined by the language itself or the popular frameworks - for ways to do things, like how to structure your REST API's. Figuring out the difference between middleware and controllers is one part of figuring out how to structure your app, but it's only one part of the puzzle. If you want the rest of the picture, sign up below to receive a template repo with the structure I use for all my Express REST API's and a post explaining in detail what logic goes where within that structure. You'll also receive all my future posts directly to your inbox!

Subscribe for the repo!

No spam ever. Unsubscribe any time.