In this post I’ll describe a powerful technique for using the TypeScript type system to verify at build time that request handlers and middlewares receive the validated types they expect and generate the expected response types. This technique makes the code more robust without adding any runtime overhead.
Unsafe request handlers
The following Express snippet defines a middleware that validates a user name passed as a URL parameter, if a corresponding User
record exists, the record gets stored in the Request
object and the request handler will read it, or else the middelware stops the processing and returns a 404:
var app = module.exports = express();
var users = [ { name: 'tj' } ];
app.param('user', function(req, res, next, id){
if (req.user = users[id]) {
next();
} else {
next(createError(404, 'failed to find user'));
}
});
app.get('/user/:user', function(req, res, next){
res.send('user ' + req.user.name);
});
Now, this works, but is not safe from the point of view of the request handler:
- there’s not guarantee that the
req
object will have auser
attribute that is in fact aUser
, with aname
(no type checking) - you could add this guarantee by adding some defensive logic in the handler that verifies thatreq.user
is actually a validUser
- this approach will work but it will add unneeded runtime overhead - there’s no guarantee that the middleware is there, suppose this code gets refactored and the middleware gets moved somewhere else - you’ll need to write unit tests to make sure that the middleware is indeed called before the request handler
These two problems can be avoided through the TypeScript type system - let’s see how.
Functional composition
The issue with the code above is that the User
object gets passed from the middleware to the handler via the Request
object (essentially an untyped hash map).
This makes impossible to carry any type information from the middleware to the handler (even tools like Flow will have a hard time tracking the type of the user
attribute).
To be able to propagate the type information we need to rethink the way we compose middlewares and handlers.
Essentially we want to do something like the following:
// middleware that extracts the User from the request
function getUserFromRequest(req: Request): User { ... };
// handler that uses the User
function userHandler(u: User): Response { ... };
// compose the two to handle a Request
userHandler(getUserFromRequest(req)); // => response
Now we just need to make this functional approach as generic and reusable as possible. Let’s see how.
Functional middlewares
I usually find it very useful to start writing code by first defining the types - once you define the types and everything looks consistent, the logic will be much easier to write.
Let’s define first a type for our middleware functions: essentially a Middleware
is a function that extracts an object of a certain type T
from a Request
. Extracting the object may require interacting with some asynchronous resource (e.g. a database) so want we actually want is a Promise<T>
. Moreover the processing of the Request
may fail, so the middleware may have to stop the processing of the request and return a response right away. Thus what we really want is a Promise<Either<Error, T>>
. 1 2
Finally, we want to have a way to set HTTP status codes and arbitrary values when responding to the request, thus we need to define a return type that we can convert to a response logic that is suitable for the Express handlers. We can accomplish this by defining a new type that lets us encapsulate arbitrary response logic:
interface IResponse {
readonly apply: (res: express.Response) => void;
}
For example, to construct instance of IResponse
for successful responses we could use this function:
function ResponseSuccess(o: string): IResponse {
return {
apply: res => res.send(o)
};
}
And for constructing error responses we could use this one:
function ResponseError(
status: number,
message: string
): IResponse {
return {
apply: res => res.status(status).send(message)
};
}
Now we can define the type for a functional version of a middleware:
type RequestMiddleware<T> = (
request: Request
) => Promise<Either<IResponse, T>>;
The type is self-descriptive, it defines a middleware as a function that takes a Request
and asynchronously returns either an IResponse
(in case of error) or a value of type T
.
We can now translate our initial example code that extracts the user
from the request into a more functional middleware:
/**
* A middleware that extracts a valid User from the
* Request and returns an error in case the user
* does not exist.
*/
function userMiddleware(
req: Request
): Promise<Either<IResponse, User>> {
const id = req.params.user;
const user = users[id];
if (user) {
// if user exist
// resolve promise with the user
return Promise.resolve(right(user));
} else {
// if the user does not exist
// resolve the promise with a 404 response
return Promise.resolve(
left(ResponseError(404, 'user not found'))
);
}
}
Functional request handlers
Now that we have RequestMiddleware
type for extracting arbitrary values from a Request
in a type safe way, how do we define a handler? Well, now handlers don’t have to extract information from a Request
anymore, so they just become pure functions that take some values and return an IResponse
.
We can now translate our initial example into a more functional handler:
/**
* A request handler that returns a User's name
*/
function getUserHandler(user: User): Promise<IResponse> {
return Promise.resolve(ResponseSuccess(user.name));
});
Note that in the case of a request handler, we don’t need an Either
anymore since the handler is always the last function to process the request, so it must produce an IResponse
(that can be a success or an error response).
Composing middlewares and request handler
Now we know how to extract values from a Request
and how to generate an IResponse
from those values. But how to we glue everything together into a function that can be used as an Express handler?
First of all we need a way to compose middlewares and handlers. In the previous example we used one single middleware to extract a value from the Request
but in practice we want to be able to chain multiple reusable middlewares that can extract several values from a single Request
.3
Let’s reason about how we could create a function that can compose an arbitrary number of middlewares with a request handler.
Starting with a single middleware we have:
function withMiddleware<T>(
middleware: RequestMiddleware<T>
)(
handler: (value: T) => Promise<IResponse>
): (req: Request) => Promise<IResponse> {
// given a handler
return handler => {
// and a request
return request => new Promise((resolve, reject) => {
// call the middleware to extract the required
// value from the request
middleware(req).then(v => {
if(isLeft(v)) {
// if the result is an error response,
// resolve the promise with the error
// response (processing of the request
// stops here)
resolve(v.value);
} else {
// if the result is a valid value
// pass it to the request handler...
handler(v.value)
// ...and resolve this promise with the
// handler response
.then(resolve, reject);
}
}, reject);
});
};
}
The withMiddleware
function looks a little bit complicated but is in fact very simple. It takes a RequestMiddleware
that extracts a value of type T
from a Request
and a function (handler
) that takes that value of type T
and returns an IResponse
.
The withMiddleware
function returns a new function that takes an Express Request
and returns an IResponse
.
We can now compose the middleware and the handler to process a Request
and have back an IResponse
:
const h = withMiddleware(userMiddleware)(getUserHandler);
// h: (Request) => IResponse
Now we need a way to transform the function returned by withMiddleware
into an Express handler:
function wrapRequestHandler(
handler: (req: express.Request) => IResponse
): express.RequestHandler {
return (request, response, _) => {
// pass the Request to the handler
handler(request).then(
// if the Promise resolves to an IResponse
// call it's apply method passing the Express
// response object
r => r.apply(response),
// if the Promise gets rejected,
// return a 500 error
e => response.status(500).send()
);
};
}
So our final code will look like this:
// define a GET endpoint
app.get('/user/:user',
// convert the type safe handler
// into an Express handler
wrapRequestHandler(
// apply the middlewares to the type-safe handler
withMiddleware(userMiddleware)(getUserHandler)
)
);
Multiple middlewares
Now that we have a way to compose a single middleware with a type safe handler to generate an Express request handler, let’s try to extend this concept to support multiple middlewares that each can extract a different value from the request.
To do that, we need to extend withMiddleware
to accept multiple middleware functions that can produce values of different types. For instance, here’s an example of withMiddleware
that can accept up to three middlewares:
function withMiddleware<T1, T2>(
m1: RequestMiddleware<T1>,
m2: RequestMiddleware<T2>
)(
handler: (v1: T1, v2: T2) => Promise<IResponse>
): (req: Request) => Promise<IResponse> {
return handler => {
return request => new Promise((resolve, reject) => {
m1(req).then(v1 => {
if(isLeft(v1)) {
resolve(v1.value);
} else {
m2(req).then(v2 => {
if(isLeft(v2)) {
resolve(v2.value);
} else {
handler(v1.value, v2.value)
.then(resolve, reject);
}
}, reject)
}
}, reject);
});
};
}
As you can see, we extend he logic by calling the middlewares in sequence, every time a middleware returns a response, we return that response - once we called all the middlewares we have all the values required by the handler. Once we reach that point we just call the handler and return its response. Here you can see an implementation that can accept up to six middlewares.
Type safe responses
Now that we have a way to make sure that request handlers actually get the parameter they need in a type safe way, let’s see if we can do the same with the responses.
Until now we only had one single type of response (IResponse
), this makes it impossible for the type system to know what kind of responses middleware and handlers generate (does the handler generate only 200
s or also 404
s?).
To have this capability we need the ability to define different response types. Let’s extend the IResponse
interface to have an associated kind
that describes the “kind” of response returned:
interface IResponse<T> {
readonly kind: T;
readonly apply: (response: Response) => void;
}
The kind
attribute doesn’t have to have an actual value, it’s just a literal type, a way for the type system to discriminate different IResponse
types.
Now we can define specific response types and their constructors:
// a successful response (HTTP 200)
interface IResponseOk
extends IResponse<"IResponseOk"> {};
function ResponseOk(value: any): IResponseOk {
return {
kind: "IResponseOk",
apply: res => res.send(value)
};
}
// a Not Found response (HTTP 404)
interface IResponseNotFound
extends IResponse<"IResponseNotFound"> {};
function ResponseNotFound(message: string): IResponseNotFound = {
kind: "IResponseNotFound",
apply: res => res.status(404).send(message)
};
We can further extend our capability of defining type safe responses by adding the type of the returned value:
interface IResponseOkJson<T>
extends IResponse<"IResponseOkJson"> {
value: T;
};
function ResponseOkJson<T>(o: T): IResponseOkJson<T> {
return {
kind: "IResponseOkJson",
apply: res => res.status(200).json(o),
value: o
};
}
Let’s see now how we can change all the functions we developed so far to make response types explicit:
// type of the response payload
interface UserJson {
name: string;
}
/**
* A request handler that returns a User's name
*/
function getUserHandler(user: User):
Promise<IResponseOkJson<UserJson>> {
const userJson = {
name: user.name
};
Promise.resolve(ResponseOkJson(userJson));
});
Now the type system knows that the request handler can only respond with an HTTP 200 response containing a UserJson
object represented as JSON.
Let’s see how the middleware changes as well:
function userMiddleware(
req: Request
): Promise<Either<IResponseNotFound, User>> {
const id = req.params.user;
const user = users[id];
if (user) {
return Promise.resolve(right(user));
} else {
return Promise.resolve(
left(ResponseNotFound("failed to find user"))
);
}
}
Now we’re sure that the userMiddleware
can only respond with a 404 in case the user is not found.
Composing middlewares with the handler should also be parametrized with response types:
// a middleware that can either return a response
// of type R or produce a value of type T
type RequestMiddleware<R, T> = (
request: Request
) => Promise<Either<IResponse<R>, T>>;
// the composition of the middlewares and the handler
// produces a function that can return responses of
// type given by the union of the middlewares response
// types (R1, R2) and the handler response type (RH)
function withMiddleware<R1, R2, T1, T2>(
m1: RequestMiddleware<R1, T1>,
m2: RequestMiddleware<R2, T2>
): <RH>(
handler: (v1: T1, v2: T2) => Promise<IResponse<RH>>
) => ((req: Request) => Promise<IResponse<RH | R1 | R2>>) {
// ... body stays the same as before
}
And so should be the wrapRequestHandler
helper:
function wrapRequestHandler<R>(
handler: (req: Request) => IResponse<R>
): express.RequestHandler {
// ... body stays the same as before
}
Conclusion
Let’s now rewrite our initial example with this new functional approach (the code looks a little verbose because I’ve made explicit all types):
const app = module.exports = express();
// the internal representation of a User
interface IUser {
name: string;
};
// the "User" database
const users: ReadonlyArray<IUser> =
[ { name: 'tj' } ];
// the middleware that looks up the User from
// the Request, note how expressive is the type
// of the middleware: we can immediately understand
// all the possible outcomes of this middleware
// from its type (and the type system knows that too)
const userMiddleware: RequestMiddleware<IResponseNotFound, IUser> =
(req) => {
const id = req.params.user;
const user = users[id];
const res = user ?
right(user) :
left(ResponseNotFound("failed to find user"));
return Promise.resolve(res);
}
// describes our API response
interface IResponseJson {
user_name: string;
};
// the request handler doesn't deal with a raw
// Request anymore, it gets all params it needs,
// already parsed and validated by the middlewares
// - look also how expressive is the type of this
// handler, we immediately know what goes in (IUser)
// and comes out (IResponseOkJson<IResponseJson>)
const getUserHandler:
(user: IUser) => Promise<IResponseOkJson<IResponseJson>> =
(user) => {
const responseJson: IResponseJson = {
user_name: user.name
};
Promise.resolve(ResponseOkJson(responseJson));
});
// now compose the middleware and the handler -
// note how the result type includes all possible
// responses (from the middleware and from the handler)
const h: (express.Request) =>
Promise<IResponseOkJson<IResponseJson> | IResponseNotFound> =
withMiddlewares(userMiddleware)(getUserHandler);
// finally, we generate the handler for Express
app.get('/user/:user', wrapRequestHandler(h));
Let’s recap what we have accomplished:
- a request middleware that takes a
Request
and produces defined response (IResponseNotFound
) and output (User
) types - a request handler with well defined input (
User
) and response (IResponseOkJson<UserJson>
) types - a function that composes the middleware and the handler into a new function that takes a
Request
and produces well defined response types (IResponseNotFound | IResponseOkJson<UserJson>
) - a wrapper that transforms the composed handler into an Express compatible request handler
In essence, we now have instructed the TypeScript compiler to verify that:
- all required parameters of the request handler get correctly extracted and validated from a
Request
by the middleware functions - all types of responses gets explicitly defined both by the middleware functions and the request handler
We know know exactly what goes into a request handler and what comes out, and it is all verified by the TypeScript compiler without having to create unit tests.
This is the real power of type systems in action!
What’s next? The next step would be to automatically generate request handlers and response types from Swagger API definitions, I’ll write about this in a new article.
You can see this technique implemented in a real world project (look under lib/utils
and lib/controllers
.
Discuss this post on Reddit and HN.
Update: fixed withMiddleware
type signature, thanks Giulio Canti for the catch.
-
Why can’t we just use
Promise<T>
since promises can carry anError
? The problem is that you can’t specify the type of yourError
, instead we want to be able to define the type of error response too. ↩ -
An
Either<T,V>
type represents either an error of typeT
or a successful value of typeV
- you can see an implementation in the fp-ts library. ↩ -
We could have just one single middleware that extracts all the values we need for a handler, but that would become impractical as we would have to create a middleware specific for the data required by each handler - it is better instead to have a way of composing multiple simple middlewares. ↩