Using Express with Strictly Typed Handlers and Responses
- First Thing's First
- Defining the Response Type
- Defining the Request Middleware
- Middleware Composition
- The Middleware Handler
- Using our Request Handler with Express
- Recap
I've recently came across a problem where I really didn't know how to approach,
which was adding strict typings onto Express middlewares and response types.
There seems to be no way to do this without messing half of @types/express
and
@types/node
:
I've posted my question on /r/typescript and Federico Feroldi (GitHub @cloudify) pointed me to his article about functional composition of Express middlewares with strict typings, but I found it extremely confusing, so this is my attempt to explain how to add compile-time type checking to your Express middlewares.
First Thing's First
We first consider the terms and the behaviour below:
- A middleware takes a raw
express.Request
instance and- returns an object with type
T
, or - end the request early because something bad happened
- returns an object with type
- A middleware handler takes a bunch of
T
s from composed middlewares and returns one final response.
We can visualize middlewares like this, where M_x
are the middlewares and
R_x
are the responses of each corresponding middleware:
Then, a middleware handler accepts responses R_x
from the middlewares and
returns one response IResponse<T>
Defining the Response Type
We realize the response of a middleware, or a composition of middlewares with
the type IResponse<T>
, where T
is a string literal to discriminate between
different IResponse
-s.
You can think of it as an intermediary type which is used internally to
represent a response, and provides an apply
function that has an
express.Response
so that you can tell express how to return an actual response
when presented the IReponse<T>
type:
Example: ResponseSuccessJson
Here's an example of a response creator that creates an IResponseSuccessJson
returning a JSON with the status 200:
Example: ResponseFailJson
And here we have the same thing but returning a status of 400:
Defining the Request Middleware
Next, we have the request middleware definition IRequestMiddleware
. A
IRequestMiddleware
is a function that takes in an express.Request
as a
parameter and returns a promise of Either
the error response IResponse<E>
or
a type R
. The Either
type is used to facilitate cases where a middleware
chain has to be stopped and a response has to be send immediately (more on this
later).
Short Interlude on the Either
Type
The Either
type is provided by the
fp-ts
library, and Either
is a strict
disjoint union type.
It is by convention that the left type is the 'failure' state while the right
type is the 'success' state. We can use helper functions like left
and right
to explicitly return the left or right type of a function.
Subsequently we can use isLeft
and isRight
to tell left and right values
apart.
Middleware Composition
Now that we have the response and the request middleware typings, we can compose
middlewares by wrapping them with withRequestMiddlewares
. If at any point of
time a middleware fails, an IReponse<E>
is returned by using the left
function, which will bail out the whole middleware chain and return a response
of IResponse<E>
.
Here's the definition and implementation of withRequestMiddlewares
, note that
the middlewares are called one after another, and if a left value is returned,
we resolve the left value and bail out from the middleware chain; otherwise,
we continue processing the next middleware.
The Middleware Handler
The middleware handler is the bridge between the the internal data types of your
application and the actual interface that the consumer of an API gets. A
middleware handler takes all the response types of the middlewares, and then
returns an IResponse<T>
which is then applied onto express.Response
.
It is also a good abstraction layer that separates internal data structures from API data structures, and also provides you a clear view of what can be returned from a composition of middlewares.
Using the definitions above, we can write a middlewareHandler
function, which
takes in two middleware results User
and Profile
, and returns a success JSON
of type CompositeResponse
:
Using our Request Handler with Express
Now that our middlewares have a handler, we want to tell Express to use and send
the resulting IResponse<T>
.
With the handler example above, we can see that requestHandler
can only return
either an IResponseFailJson
or a IResponseSucessJson
. However, when an
internal error (like TypeError
) is thrown anywhere in the middleware chain, it
is automatically caught by the error
function and a response type of
ResponseErrorInternal
is returned to the consumer.
Using the predefined requestHandler
with Express is as easy as wrapping the
request handler with wrapRequestHandler
:
Recap
To recap, we have:
- A bunch of middlewares that can return
Either<IResponse<E>, R>
- A handler function that takes in the result
R
of each middleware and returns oneIResponse<T>
- A
withRequestMiddlewares
function which maps the results of middlewares onto the handler function, and bails if anE
type is emitted in any middleware - A
wrapRequestHandler
function which calls and appliesIResponse<T>
ontoexpress.Request
via theapply
function, catches other errors and emits them as anResponseErrorInternal