-
Notifications
You must be signed in to change notification settings - Fork 6
Express usage guide
You may have noticed that rest-ts-express
does a little bit more than simply adding types to regular express route handlers. In fact, it implements a common design pattern that leverages promises to make your code more robust.
A regular express route handler usually looks like this:
app.get('/some/endpoint', function handler(req, res, next) {
// Do some processing with the `req` object
// Then, if we are able to deal with this request now:
res.send(something);
// Or, if we don't know what to do, just forward to the next middleware
next();
});
This design, has some serious drawbacks:
First, we have to explicitly state the HTTP method and path of our handler. Makes sense after all this is a web server. However, when building web applications, it helps to abstract away the details of the HTTP protocol and focus on pure business logic.
Second, there could be an execution path in the handler which doesn't call any of next
or res.end
, leading to a "hanging" response. This is a very common problem for express newbies and seniors alike (except the latter are able to deal with it more efficiently).
Finally, the res
object is too powerful for common API logic handlers. This power comes in handy when you want to do data streaming or other fancy applications. But most of the time, you just want to send a data object using some encoding (usually JSON). For those cases, the extra complexity of the res
object leads to programming mistakes, it gets in the way of the readability of your code and makes it hard to provide the type-level safety Rest.ts aims to offer.
Bottom line: Raw express handlers are low-level. They are very powerful and let you do anything that is supported by HTTP, but this extra complexity is a barrier to establishing proper software engineering discipline. 99% of API servers don't need this much power and can profit from a higher-level abstraction with stronger guarantees.
The first thing rest-ts-express
does is to let you define a router based on the API definition. This provides strong type checking, proper IDE auto-completion and raises the level at which you are working by removing the superfluous and keeping the essential.
import { buildRouter } from 'rest-ts-express';
app.use(buildRouter(myAPIDefinition, (builder) => builder
.countKittens((req) => {
// snip
})
.getKitten((req) => {
// snip
})
));
This code focuses on the business logic of your router, is easy to understand and is super type-safe: it won't compile if you forgot a handler, or if you typo another. The req
parameter contains type information about the request's body, query parameters and path parameters, and the function checks that you send the correct DTO from your handler (more on that later).
Should you ever need to verify the HTTP method or path associated with a specific handler, a single click on the handler name takes you to the definition of that endpoint, with not only the HTTP method and path (and path parameters), but also the query parameters, response type, potential request body type, etc...
ℹ️Don't like the builder style shown above?
rest-ts-express
provides an alternative method, createRouter with a less awkward syntax. Beware that it isn't as safe and IDE-friendly asbuildRouter
.
Consider this: a handler takes some input data and returns something. Hold on, isn't that just the definition of a function? Why wouldn't we simply return the response instead of calling a method (send
) on an input parameter (res
)?
function handler(req, res) {
// Do some processing with the `req` object
// Then, if we are able to deal with this request now:
return something;
// Or, if we don't know what to do, just forward to the next middleware
return undefined;
}
This would be dangerous in JavaScript because the statement return something
could in fact return undefined
if for some reason something
was to contain undefined
. However, in TypeScript this is not an issue! The compiler makes sure something
will never contain undefined
, and we end up with a simpler programming model which still satisfies our requirements!
As you may have guessed, express wouldn't be that popular if it was a complete bad design. In fact, the (req, res, next)
prototype was designed specifically to support asynchronous programming.
Take this example:
function(req, res, next) {
fs.readFile(someFile, (err, data) => {
if(err) return next(err);
res.send(data);
});
}
But one major change has been introduced to JavaScript since express came about: Promises. We can have our handler return a promise instead of some raw data, and rest-ts-express
will know that it has to wait before it can proceed (ie. either send
some data or continue to the next
middleware).
function(req, res) {
return new Promise((resolve, reject) => {
fs.readFile(someFile, (err, data) => {
if(err) return reject(err);
resolve(data);
});
});
}
You might wonder: This just made our code even more complex didn't it?
In a sense yes. We had to adapt a classic callback-based node API to a promise. This is usually a bit verbose, although there are libraries out there that can help. However, callback-based APIs are losing speed in favor of promise-based APIs, that is, functions which return a promise instead of taking a callback parameter (sounds familiar?).
Take axios for instance, say we use it (or rest-ts-axios
) to query some service, here is what the code would look like in classic express style:
function(req, res, next) {
myService.getAmountOfKittens(req.params.cuteness).then(
function success(nb_kittens) {
res.send(nb_kittens);
},
function failure(err) {
next(err);
}
);
}
And now, with rest-ts-express
:
function(req) {
return myService.getAmountOfKittens(req.params.cuteness);
}
Neat, isn't it? Because getAmountOfKittens
returns a Promise, we can simply forward it.
This example was trivial because we didn't have to do any processing on the data returned by getAmountOfKittens
. A more realistic example would be:
function(req) {
return myService.getAllKittens(req.params.cuteness)
.then((kittens) => kittens.length);
}
Get some of that functional programming goodness, please.
The async/await
aficionados can have their lot of fun as well:
async function(req) {
const kittens = await myService.getAllKittens(req.params.cuteness);
return kittens.length;
}
And now, the icing on the cake: We can easily type-check the return type of that function, and make sure that it matches that of the API specification.
function(req) {
return myService.getAllKittens(req.params.cuteness); // Type ERROR: type `Kitten[]` is not assignable to `number`
}
The RouteHandler API documentation is a dense reference covering all use cases of route handlers, perfect for users already familiar with the basics.