As you've already described in the question, these three are very similar concepts, and it can be challenging to decide in many cases, depending on your preference.
But I can outline the differences:
Interceptors
Interceptors can access the request/response before and after calling the route handler.
Registration
@UseInterceptors()directly in controller classes with controller or method scope- Global scope
app.useGlobalInterceptors()inmain.ts
Examples
- LoggingInterceptor: Request before the route handler and after its result. Measure the time required.
- ResultMapping: Convert
nullto[]or wrap the result in a response object:users→{users: users}
Conclusion
Compared to middleware, I prefer registering closer to the route handler. However, there are some limitations—for example, when you send a library-specific response object from the route handler, you cannot set the response code or modify the response using interceptors; see documentation. @Res()
Middleware
Middleware is called only before calling the route handler. You can access the response object but not the result of the route handler. They essentially implement middleware functionality.
Registration
- Within modules, selecting relevant routes is highly flexible (using wildcards, by method, etc.)
- Global scope
app.use()inmain.ts
Examples
- FrontendMiddleware: Redirect all routes except API to
index.html; see this thread - You can use any existing Express middleware. There are many libraries, such as
body-parserormorgan
Conclusion
Middleware registration is highly flexible—for example, applying to all routes except one. However, since they are registered within modules, when reviewing their methods, you might not realize they apply to your controllers. You can also leverage all existing Express middleware libraries, which is great.
Exception Filters
Exception filters are called after the route handler and interceptors. They are the last place to modify the response before it is sent.
Registration
@UseFilters()directly in controller classes with controller or method scope- Global scope
app.useGlobalFilters()in yourmain.ts
Examples
- UnauthorizedFilter: Map to user-friendly messages
- NotFoundFilter: Map all not-found routes (not part of your API) to your
index.html.
Conclusion
The primary use case for exception filters is providing understandable error messages (hiding technical details). However, there are creative uses: when providing a single-page application, all routes except API routes should typically be redirected to index.html. Here, you can redirect to NotFoundException. Some might find this clever, while others might consider it old-fashioned. Your choice. ;-)
So the execution order is: middleware -> interceptors -> route handler -> interceptors -> exception filters (if an exception is thrown).
For these three tools, you can inject other dependencies (such as services, etc.) into their constructors.