2.5 Middleware
The final lesson in the Server-side programming unit concerns
itself with middleware, which is powerful abstraction commonly
used in software applications. Whether it's rate limiting, circuit
breaking, or observability, the middleware abstraction decouples
these features from the core application implementation and can be
used to apply these features to all requests unconditionally.
What is middleware?
Middleware is a fairly overloaded term, so it's important that we clarify that we're primarily concerned with middleware in distributed applications.
In this particular definition, middleware is functionality that sits between the client and the API that they're interacting with. Note that middleware is often (but not always) implemented in the same server process as the API and therefore wraps the API's core functionality. Implementing a rate limiter in Redis is a fascinating counterexample of a middleware that delegates to an external service (or database in this case).
To drive this point in, middleware is yet another example of an abstraction layer that can be visualized like an onion, where the core of the onion is the application endpoint's business logic, and the layers surrounding it make up the middleware.
Middleware in Go
Each programming language has a unique way to construct, define, and instrument
middleware in your application. The Issue Tracker application takes advantage
of the chi HTTP framework that provides a handy chi.Mux.Use method to
easily configure middleware, but it helps to see how this works under the hood.
Using the standard net/http library, a no-op, pass-through middleware can be implemented with the following:
You'll notice that we can write code that executes both before or after the
call is made to the next middleware in the chain. For example, if we want to
print a message when a request is received and after the request was served, we
can redefine the nopMiddleware as a printerMiddleware like so:
Middleware chain
Given that each of these middleware functions actually implements the http.Handler
interface, either of them could be the next in the next.ServeHTTP call, thus
creating an ordered chain! For example, we could instrument our application so that
the nopMiddleware executes, then the printerMiddleware executes, and finally the
business logic http.Handler implement by the Issue Tracker application before
the response is propagated back through the chain in the reverse order.
You will implement this in Assignment 4, so this is intentionally left as an exercise to the reader.
Practical use cases
The scope of a middleware is fairly small, but its use cases are only limited by your creativity. Business logic can be tremendously simplified by moving specific functionality into a chain of middleware.
For example, if your application needs to validate client requests based on the
parameters they provide (e.g. the title of the issue should only be 64 characters),
you could roll out a validateMiddleware that handles this for you.
Regardless of the implementation of your rate limiter, you could add a rateLimitMiddleware
that delegates to rate limiter service and enforce it in the chain. This could similarly
be done for a cache implementation, which is something we'll discuss in the RPC unit!