The goal of this project is to test the limits of the typescript backend frameworks. The project will be a simple CRUD API with a single entity. Each framework will be tested for the following:
- Type safety
- Community support
- Developer support
- Developer experience
- Documentation
- Performance
The type safety is a major factor in the evaluation of the frameworks. Most of our projects at Poly were written in typescript, and while we do like the language, we have had some issues with type safety on several backend frameworks. We hope to find a framework that is both completely type safe and has a good developer experience.
Each project will try to have theses features:
- An auth middleware that will add a user to the request / context so that routes can access it (should be typed)
- Route
authors/:id
with aPUT
method that takes a body and a param and will return an author with a combination of the body, the param and the user from the auth middleware (everything should be typed) - A properly constructed openapi file
- The openapi file should be easily configurable
- The type between the openapi file and the route should be safe
- A
/reference
route that will serve a scalar ui website thanks to the openapi file
I need to use a third party library for validation and a complete type safety:
-
⛔ The hono-openapi does not type check the result types and the schema given by the openapi file.
-
✅ The Zod OpenAPI generates the openapi file from the zod schema and the response type is safe between the route and the schema which is a good thing, but the library is a bit verbose.
Easy to setup / use and type safe everywhere.
It looks like scalar may have a problem with 'examples' in params.
example of openapi file given by Elysia:
"parameters": [
{
"description": "The ID of the author",
"examples": [
"12"
],
"schema": {
"type": "string",
"minLength": 1
},
"in": "path",
"name": "id",
"required": true
}
],
example of what is given by Hono
"parameters": [
{
"schema": {
"type": "string",
"minLength": 3,
"description": "The ID of the user",
"example": "1212121"
},
"required": true,
"name": "id",
"in": "path"
}
],
Everywhere else the examples field works fine but not in the params, problem with scalar ?
Elysia may have an older version of Typebox as dependence, so if our project wants to use the latest release of Typebox, we will have to create an alias in package.json to use this version. Also, keep in mind that it wont be compatible with the typebox Elysia types.
The distinction between the query, the params and the body is not clear.
The hot reloading is not working properly ?
Based on express so very slow.
As of 24/12/2024, the framework looks a bit incomplete:
- Cannot add objects to context
- OpenAPI is still experimental
Looks promising.
Terrible doc.
Stopped testing after trying to find a library that would properly type the routes, found one with 60 github stars but way less complete than the express zod api one. koa-zod-router
Hard time to find how to properly type a object added to the context, don't think their is a good way to do it.
Easy to use, but the it suffers from the same problem as nestjs: the decorators and the typescript function are not safe together.
5 min to shut down the server ?
Based on express so very slow.
Adding context is not easy and not truly typed.
OpenAPI is not easily configurable (BY COMMENT !!) AND not type safe.
Some requests are failing when benching it.
Was really easy to setup and make it work.
In my opinion, chained decorators are not easy to read.
@ApiResponse is not typed with the return type of the route.
Doc looks great.
You can give custom "context" thanks to the custom param decorator but the type is not well checked.
Based on express so very slow.
body typed | param typed | context typed | open api | open api typed end to end | front end client | perf (avg request) | |
---|---|---|---|---|---|---|---|
hono | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 28.4k |
elysia | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 45.3k |
express-zod-api | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 5.2k |
encore.ts | ✅ | ✅ | ⛔ | ⛔ | ⛔ | ✅ | 8.5k |
fastify | ✅ | ✅ | ⛔ | ✅ | ✅ | ⛔ | 10.3k |
tsed | ✅ | ✅ | ⛔ | ✅ | ⛔ | ⛔ | 3.4k |
adonisJs | ✅ | ✅ | ⛔ | ✅ | ⛔ | ⛔ | 6k |
nestjs | ✅ | ✅ | ⛔ | ✅ | ⛔ | ⛔ | 4.3k |
We can see that express based frameworks are slower than the others.
We have three frameworks which can achieve the type safety we were looking for and are express-like (chained functions, middlewares, etc):
- hono
- elysia
- express-zod-api
Hono has 20k github stars, and it looks like it has a solid community / plugins. Also; the performance is good for a javascript framework. In order to have the complete type safety, we need to use a third party library Zod OpenAPI which is a bit verbose.
Elysia has 11k github stars, has a bit less community than hono. It performs very well, the type safety is complete, and completely integrated within the framework. It was initially made for Bun but it is now an agnostic framework.
Express-zod-api has 600 github stars, is itself a third party library, has mediocre performance because it is based on express. The type safety is complete. The advantage of this framework is that it is based on express, we can use the insane amount of plugins available for express.
We can see that when a framework is more architecture opinionated, the type safety is not complete. The frameworks using decorators like nest.js cannot achieve a complete type safety.
In my opinion encore.ts is a promising framework. It has 8.3k stars, the type safety is not complete yet and some features are missing but it looks like it can provide a good developer experience.
Some of these frameworks are huge, and I did not graded them on all the features they offer. Obviously complete type safety and performance is not the only thing we have to look for when we are choosing a typescript backend server.
Also the bench is done on my computer and is based on a simple route. The performance can be different with a more complex route, or with a database, with more features, etc.