A very fast alternative HTTP server to Express, with simple routing and middleware support, that is compatible with Node.js 18 or later.
Install [↑]
Execute the following command from your project folder, where your
package.json file is stored:
npm install --save @egomobile/http-serverIf you want to lookup types, also install the Node Types:
npm install --save-dev @types/nodeUsage [↑]
Quick example [↑]
importcreateServer,{buffer,params,query}from"@egomobile/http-server";asyncfunctionmain(){constapp=createServer();// POST request for / route// that uses the middleware buffer(), which loads the// whole request body with a limit of 128 MB by default// and writes the data to 'body' prop of 'request' object// as Bufferapp.post("/",[buffer()],async(request,response)=>{constname: string=request.body!.toString("utf8");response.write("Hello: "+name);// no response.end() is required here});// parameters require a special path validator here// s. https://github.com/lukeed/regexparam// for more information about the string formatapp.get(params("/foo/:bar/baz"),async(request,response)=>{response.write("BAR: "+request.params!.bar);});// parse query parameters from URL// and write them to 'query' prop of 'request' objectapp.get("/foo",[query()],async(request,response)=>{// request.query => https://nodejs.org/api/url.html#class-urlsearchparamsresponse.write(" BAR: "+request.query!.get("bar"));response.write(" BAZ: "+request.query!.get("baz"));});awaitapp.listen();console.log(`Server now running on port ${app.port} ...`);}main().catch(console.error);Middlewares [↑]
To enhance the functionality of your handlers, you can setup global or route specific middlewares.
For more details, have a look at the wiki page.
Controllers [↑]
The module provides tools, like decorators, functions and classes, that helps to setup routes and their behavior on a quite simple and high level.
Have a look at the wiki page for detailed information.
Error handling [↑]
importcreateServerfrom"@egomobile/http-server";asyncfunctionmain(){// ...// custom error handlerapp.setErrorHandler(async(error,request,response)=>{consterrorMessage=Buffer.from("SERVER ERROR: "+String(error),"utf8");if(!response.headersSend){response.writeHead(400,{"Content-Length": String(errorMessage.length),});}response.write(errorMessage);response.end();});// custom 404 handlerapp.setNotFoundHandler(async(request,response)=>{constnotFoundMessage=Buffer.from(`${request.url} not found!`,"utf8");if(!response.headersSend){response.writeHead(404,{"Content-Length": String(notFoundMessage.length),});}response.write(notFoundMessage);response.end();});app.get("/",async(request,response)=>{thrownewError("Something went wrong!");});// ...}main().catch(console.error);Pretty error pages [↑]
A nice example is, to use Youch! by Poppinss.
It prints pretty error pages in the browser:
importcreateServer,{prettyErrors}from"@egomobile/http-server";importyouchfrom"youch";asyncfunctionmain(){// ...app.setErrorHandler(async(error,request,response)=>{consthtml=Buffer.from(awaitnewyouch(error,request).toHTML(),"utf8");if(!response.headersSent){response.writeHead(500,{"Content-Type": "text/html; charset=UTF-8","Content-Length": String(html.length),});}response.end(html);});app.get("/",async(request,response)=>{thrownewError("Oops! Something went wrong!");});// ...}main().catch(console.error);A possible result could be:
Testing [↑]
With decorators @Describe() and @It(), you can write automatic (unit-)tests, realized by any framework you want.
This example shows, how to implement tests with SuperTest (if you want to see a more detailed description of this feature, you can visit the wiki page):
Controller [↑]
import{Controller,ControllerBase,Describe,GET,IHttpRequest,IHttpResponse,It,}from"@egomobile/http-server";
@Controller()
@Describe("My controller")exportdefaultclassMyControllerextendsControllerBase{
@GET("/foo/:bar")
@It("should return '{{body}}' in body with status {{status}} when submitting parameter {{parameter:bar}}",{expectations: {body: "BUZZ",status: 202,},parameters: {bar: "buzz",},})asyncindex(request: IHttpRequest,response: IHttpResponse){response.writeHead(202);response.write(request.params!.bar.toUpperCase());}}Initialization [↑]
importassertfrom"assert";importsupertestfrom"supertest";import{createServer}from"@egomobile/http-server";constapp=createServer();// event, that is executed, if a test is requestedapp.on("test",async(context)=>{const{
body,
description,
escapedRoute,
expectations,
group,
headers,
httpMethod,
server,}=context;try{process.stdout.write(`Running test [${group}] '${description}' ... `);// prepare request ...// HTTP method ...letrequest=supertest(server)[httpMethod](escapedRoute);// request headers ...for(const[headerName,headerValue]ofObject.entries(headers)){request=request.set(headerName,headerValue);}// send itconstresponse=awaitrequest.send(body);assert.strictEqual(response.statusCode,expectations.status);// maybe some more code checking headers and// body data from `expectations` ...process.stdout.write(`✅\n`);}catch(error){process.stdout.write(`❌: ${error}\n`);}});// run testsawaitapp.test();// alternative://// if you set `EGO_RUN_SETUP` to a truthy value like `1`// the server does not start listening, instead it simply// runs `app.test()`//// await app.listen();Benchmarks [↑]
Express | fastify | polka | @egomobile/http-server | |
|---|---|---|---|---|
Express | - | 93% | 39% | 30% 🐌 |
fastify | 107% | - | 43% | 32% 🐢 |
polka | 256% | 238% | - | 76% 🐇 |
@egomobile/http-server | 337% 🚀🚀🚀 | 314% 🚀🚀 | 132% 🚀 | - |
The following benchmarks were made with wrk on the following machine, running Node v16.13.2:
Machine:
- MacBook Pro (16", 2021)
- CPU: Apple M1 Max
- Memory: 64 GB
- OS: MacOS 12.1
Command: wrk -t8 -c100 -d30s http://localhost:3000/user/123
Express:
=============
Running 30s test @ http://localhost:3000/user/123
8 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 3.56ms 674.79us 14.59ms 90.47%
Req/Sec 3.39k 224.41 5.11k 75.04%
809164 requests in 30.03s, 118.84MB read
Requests/sec: 26947.30
Transfer/sec: 3.96MB
Fastify:
=============
Running 30s test @ http://localhost:3000/user/123
8 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 3.32ms 0.95ms 19.41ms 85.25%
Req/Sec 3.64k 280.76 4.87k 76.38%
869871 requests in 30.03s, 142.69MB read
Requests/sec: 28971.44
Transfer/sec: 4.75MB
Polka:
===========
Running 30s test @ http://localhost:3000/user/123
8 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.39ms 289.29us 13.20ms 91.15%
Req/Sec 8.66k 1.26k 10.67k 59.55%
2074873 requests in 30.10s, 259.22MB read
Requests/sec: 68930.81
Transfer/sec: 8.61MB
@egomobile/http-server:
============================
Running 30s test @ http://localhost:3000/user/123
8 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.05ms 220.64us 13.11ms 85.16%
Req/Sec 11.44k 1.39k 18.48k 81.16%
2737095 requests in 30.10s, 341.95MB read
Requests/sec: 90922.13
Transfer/sec: 11.36MB
Here is the test code, used recording the benchmarks.
Credits [↑]
The module makes use of:
- Ajv
- Filtrex by Michal Grňo
- joi by Sideway Inc.
- js-yaml by Nodeca
- minimatch by isaacs
- regexparam by Luke Edwards
- Swagger UI and @open-api
Documentation [↑]
The API documentation can be found here.
See also [↑]
- @egomobile/api-utils - Extensions for this module, helping realizing REST APIs
