Recently, in a job interview, I was faced with the following challenge: to create a web framework from scratch, with the fewest possible dependencies. This was one of the most fun and educational processes I've ever been through, and even after the testing phase was over, I continued working on the project that resulted in unicorn.web: my own web framework.
About Bun...
After completing the process, I chose to switch the interpreter under the node framework to bun. First because I wanted to try this new and talked-about interpreter, but also because it promised to increase the performance of any JavaScript application by up to 5x, which was very interesting to me.
The first thing I noticed was how the development experience with bun is superior to that of node. With it, you can run TypeScript files natively, without any additional configuration. It has a really phenomenal module resolver that makes it much easier to import and export files, and a fairly objective and easy-to-understand documentation. It also comes with a test module that is very similar to Jest (but infinitely faster). Even if Bun didn't deliver the performance it promises, these features alone would make it worthwhile and saved me a few hours of setup work.
In addition, its performance is really impressive, its initialization is extremely fast, and a simple load test showed the ability to hold up to 40,000 requests per second on my machine.
About the framework
Just to be clear... this is not a framework to be used in a production environment (yet). The idea here was to write something simple, very easy to learn and use. A lightweight framework for testing concepts and developing small projects.
In practice, this meant that the user should be able to deploy an API with just 3 or 4 lines, and that goal was achieved! After installing Bun and adding the framework as a dependency, with just three lines you can create a simple GET route.
bun add @unicorn.web/core
import { UnicornServer } from '@unicorn.web/core'
const server = new UnicornServer();
server.get('/health', () => new Response("I'm working!"));
server.serve(3000);
If you need more complex routes, simply add the logic inside the callback. It is also possible to extract data from the request (query, body, params, and headers) by adding a Context parameter
import { UnicornServer } from '@unicorn.web/core'
const server = new UnicornServer();
server.get('/hi', (ctx: Context) => {
return new Reponse(`Hi ${ctx.query.name}!`);
});
server.serve(3000);
Finally, to run your application, a simple bun run index.ts solves all your problems.
Conclusion
More than publish the framework, the goal here was to understand once and for all what runs under the frameworks I use at work and on a daily basis. Understand how to deploy a "raw" web server, how to process the requests that this server receives, where to find the information contained in these requests, and much more.
Challenges arose along the way. A good example: how to match routes that have patterns in their definitions (Example: GET /person/:id) and extract their parameters? A lesson in regex and data structures like tries.
Certainly, venturing into this more "low-level" universe and abandoning the abstractions that frameworks like nest or adonis provide us with was very positive and taught me a lot. As a backend developer, I now understand better how web servers work, what HTTP actually is, and what is behind the whole process of communication between clients and servers in the web universe.
If anyone wants to give suggestions and contribute to the project, it is completely open source, feel free to open issues and send pull requests (or comment here even rs).
Top comments (2)
Flames all around with this! 🔥🔥
Impressive!