npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

mini-express-server

v1.1.7

Published

A short implementation of a web server based in express architecture using build-in node modules

Downloads

4

Readme

mini-express-server

A minimal web server implementation based on express architecture uses only built-in node modules like path, http, http2, and fs. The core class is AppServer, which creates an instance of Server calling createServer from node:http and can create a server that implements a http2.0 protocol using node:http2. It listens to incoming requests and, based on the request method (GET, POST, PUT, DELETE, PATCH, HEAD). If the method is not supported, the Server returns a 405 status code with a "Not allowed" response.

installation

npm i mini-express-server --save

The implementation maintains the same architecture of express, where you can configure several middlewares for every route. Also, you can register global middlewares. See the example in Typescript

Usage

Basic usage

import AppServer, { IRequest, IResponse } from 'mini-express-server';

const app: AppServer = new AppServer();
const morgan = require('morgan');
const helmet = require("helmet");
const cors = require('cors')
const bodyParser = require("body-parser");

const port: number = +(process?.env?.PORT || 1234);

///////You can use morgan middleware for logging as in express ////////
app.use(morgan('dev'));
app.use(cors()); // Using the popular cors
app.use(helmet()); // Using the popular helmet
app.use(bodyParser.json()); // Using body-parser
app.use(bodyParser.urlencoded({ extended: false }));


app.get('/', (req: IRequest, res: IResponse) => {
  console.log('Hello World');
  return res.status(200).text('Hola mundo');
});

app.get('/api', (req, res) => {
  const { query, params, body, headers } = req;
  res.status(200).json({ query, params, body, headers });
});

app.listen(port, (address: any) => {
  console.log('Server listening on: ', address);
});

Definig multiples middlewares

...
/// Declaring middlewares /////////
const midd1: IMiddleware = (req: IRequest, res: IResponse, next) => {
  req.context.date = new Date();
  next();
};
const midd2: IMiddleware = (req: IRequest, res: IResponse, next) => {
  req.context.user = { name: 'Example', token: '454as54d5' };
  next();
};

app.get('/', midd1, (req: IRequest, res: IResponse) => {
  console.log('Hello World');
  return res.status(200).text('Hello World');
});

app.get('/api', midd1, midd2, (req, res) => {
  const { query, params, body, headers, context } = req;
  console.log(context);
  res.status(200).json({ query, params, body, headers, context });
});
....

Example of console.log when we hit the endpoint "/api" in the above example

Error Handling

By default, the library catches all the errors inside the middleware and passes them to an internal global error handler, where the error message is returned to the client. Also, as in express, you can give the next function the object representing the error.

import AppServer, { IMiddleware, IRequest, IResponse, ServerError } from 'mini-express-server';

const app: AppServer = new AppServer();
const morgan = require('morgan');
const port: number = +(process?.env?.PORT || 1234);

app.use(morgan('dev'));

app.get(`/error/1`, (req, res, next) => {
  next(new ServerError(400, 'Custom Error', [{ message: 'Custom error to test' }]));
});

app.get(`/error/2`, async (req, res, next) => {
  let asyncOp = new Promise((_, reject) => {
    setTimeout(() => {
      reject('There was an error');
    }, 1000);
  });
  await asyncOp;
});

app.listen(port, (address: any) => {
  console.log('Server listening on: ', address);
});

You can configure your custom Error handler

import AppServer, { IMiddleware, IRequest, IResponse, ServerError } from 'mini-express-server';

const app: AppServer = new AppServer();
const morgan = require('morgan');
const port: number = +(process?.env?.PORT || 1234);

app.use(morgan('dev'));

app.get(`/error/1`, (req, res, next) => {
  next(new ServerError(400, 'Custom Error', [{ message: 'Custom error to test' }]));
});

app.get(`/error/2`, async (req, res, next) => {
  let asyncOp = new Promise((_, reject) => {
    setTimeout(() => {
      reject(
        new ServerError(429, 'Too many requests', [
          'To many request for this user',
          'Clean cookies',
        ])
      );
    }, 1000);
  });
  await asyncOp;
});

///////// Custom Error handler //////////////////////////////
app.setErrorHandler((req, res, error) => {
  console.error('There is an error: ', error.message);
  let code = error.code && !isNaN(parseInt(error.code)) ? error.code : 500;
  res.status(code).json({ message: error.message, error: true, meta: error.meta });
});

app.listen(port, (address: any) => {
  console.log('Server listening on: ', address);
});

Example response when we hit the endpoint "/error/2" in the above example

res-2

Static files Server

The mini-express-server also can serve static files and is quite similar to express. But here, using the method setStatic. The example below shows how to set different endpoints for serving static files.

import AppServer, { IMiddleware, IRequest, IResponse, ServerError } from 'mini-express-server';

const app: AppServer = new AppServer();
const morgan = require('morgan');
import path from 'node:path';

const port: number = +(process?.env?.PORT || 1234);

app.use(morgan('dev'));

...
/// first the endpoint for static files, the other the root path of the directory that contain the files
app.setStatic('/static', path.join(__dirname, '..', 'public'));
app.setStatic('/storage', path.join(__dirname, '..', 'storage'));
...

app.listen(port, (address: any) => {
  console.log('Server listening on: ', address);
});

Stackblitz example of serving static files from different sources

https://stackblitz.com/edit/node-u2qygg?file=index.js

Router

Since version 1.0.8, we have introduced the class Router that allows the creation of modular, mountable route handlers. A Router instance is a complete middleware and routing system. With that, you can split your app logic, enabling the separation of concerns and improving the modularity of your app. Here an example of creation of CRUD operation over a class USER

const { Router, ServerError } = require("mini-express-server");
const { authorizationMidd } = require("../middlewares");
const { User } = require("../models");

const router = new Router(); /// Creating a instance of Router class

///////// Custom middleware to get the user by the param :/userId ///////////
const userByIdMiddleware = async (req, res, next) => {
  const userId = req.params.userId;
  const user = await User.getUserById(userId);
  if (!user) throw new ServerError(404, `User with id=${userId} not found, try with other id`);
  req.context.user = user;
  next();
};

router.get("/", async (req, res) => {
  let users = await User.getListUsers();
  res.status(200).json({ data: users });
});

router.get("/:userId", userByIdMiddleware, async (req, res) => {
  res.status(200).json({ data: req.context.user });
});

router.post("/", async (req, res) => {
  let data = req.body;
  let newUser = await User.createUser(data.name, data.lastName, data.age);
  res.status(201).json({ data: newUser });
});

router.put("/:userId", userByIdMiddleware, async (req, res) => {
  let user = req.context.user;
  let data = req.body;
  user = await User.editUser(user);
  res.status(201).json({ data: user });
});

router.patch("/:userId", userByIdMiddleware, async (req, res) => {
  let user = req.context.user;
  let data = req.body;
  user = await User.editUser(user);
  res.status(201).json({ data: user });
});

router.delete("/:userId", authorizationMidd, async (req, res) => {
  if (req.loggedUser.id != req.params.userId) {
    throw new ServerError(403, "Not allowed", ["you can edit other users"]);
  }
  let user = req.loggedUser;
  await User.deleteUser(user.id);
  res.status(200).json({ status: "Ok" });
});

module.exports = router;

In the main file fo your app you only have to do this:

...
const app = new AppServer();
const userRouter = require("./routes/user.js");
const port = 1234;
...
app.use("/api/user", userRouter);

Http2 Server

The constructor of the class AppServer can receive an options object where you can pass through the createServer function, and you can configure here options like:

keepAlive: true // default value
connectionsCheckingInterval: 30000 // default value
keepAliveInitialDelay: 0 // default value
keepAliveTimeout: 5000 // default value
maxHeaderSize: 16384 // default value
noDelay: true // default value
httpVersion: "HTTP1" //default value

In order to create a server with http2.0 protocol, you only have to pass the corresponding configuration; see the example below:

const { AppServer } = require("../build/cjs/index");
const app = new AppServer({ httpVersion: "HTTP2" })


app.get("/", (req, res) => {
    res.status(200).json({ message: "Hello from server", method: "GET" })
})

app.post("/", (req, res) => {
    res.status(200).json({ message: "Hello from server", method: "POST", body: JSON.parse(req.body) })
})

app.put("/", (req, res) => {
    res.status(200).json({ message: "Hello from server", method: "PUT", body: JSON.parse(req.body) })
})

app.get("/api", (req, res) => {
    const { query, params, body, headers } = req;
    res.status(200).json({ query, params, body, headers });
})
app.get("/api/:param1", (req, res) => {
    const { query, params, body, headers } = req;
    res.status(200).json({ query, params, body, headers });
})

app.listen(8000, (address) => {
    console.log("Server listening: ", address)
})

All the previous features the AppServer has will remain for the new server configuration; it only changes the protocol. You can use the router, the middlewares etc. also for testing this server the package provides a fetchClient http 2.0, see the example below:

const { fetchHttp2 } = require("mini-express-server");

async function main() {
    const API_HOST = "http://127.0.0.1:8000"
    try {

        let res = await fetchHttp2(API_HOST);
        let data = JSON.parse(res.data);
        console.log("Response on res: ", data);
        // Response on res:  { message: 'Hello from server', method: 'GET' }

        res = await fetchHttp2(API_HOST, {
            method: "POST",
            body: JSON.stringify({ name: "Jose", age: 27, date: new Date() }),
        });
        data = JSON.parse(res.data);
        console.log("Response on res: ", data);
        /**
        Response on res:  {
                message: 'Hello from server',
                method: 'POST',
                body: { name: 'Jose', age: 27, date: '2023-02-10T13:30:51.992Z' }
            }
        */
        res = await fetchHttp2(API_HOST, {
            relativePath: "/api/exampleFullEndpoint/?limit=10&offset=5&status=enabled",
        })
        data = JSON.parse(res.data);
        console.log("Response on res: ", data);
        /**
         * Response on res:  {
            query: { limit: '10', offset: '5', status: 'enabled' },
            params: { param1: 'exampleFullEndpoint' },
            body: '',
            headers: {
                ':path': '/api/exampleFullEndpoint/?limit=10&offset=5&status=enabled',
                ':method': 'GET',
                ':authority': '127.0.0.1:8000',
                ':scheme': 'http'
            }
        }
         */

    } catch (e) {
        console.log("Error: ", e);
    }
}

main();

Benchmarking

We perform stress tests on an example API and compare it to express. We used the apache tool ab (Server benchmarking tool), and in the following setup, we got better results than ExpressJs. The setup in which we made the tests is two endpoints where one returns a JSON object with the params, body, query, and header. The other return web pages in a dynamic parameter pass in the route.

Basic api setup for mini-express-server and express


...
const app = new AppServer()
const port = 1234;

app.use(morgan("common"));
app.use(cors());
app.use(helmet());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

app.get(`/api`, (req, res) => {
    const { query, params, body, headers } = req;
    res.status(200).json({ query, params, body, headers });
})

//////////////////////////// RENDER WEB PAGE //////////////////////////
//////////////////Configuring the static route//////////////////////
app.setStatic("/api/web/static", path.join(__dirname, ".", "static"))

app.get(`/api/web/:page/`, (req, res, next) => {
    let page = req.params.page;
    let pagesRootPath = path.resolve("src", "pages");
    fs.readdir(pagesRootPath, { encoding: "utf8" }, (err, files) => {
        if (err) {
            next(err);
        } else {
            let fileFound = files.find((file) => file == (page + ".html"))
            if (fileFound) return res.status(200).sendFile(path.resolve(pagesRootPath, fileFound));
            return next(new ServerError(404, "Page not found", [{ "websites": files }]));
        }
    })
})
///////////////////////////////////////////////////////////////////////////////
app.setErrorHandler((req, res, error) => {
    console.error("THere is an error: ", error);
    let code = error.code && !isNaN(parseInt(error.code)) ? error.code : 500;
    res.status(code).json({ message: error.message, error: true, meta: error.meta })
})

app.listen(port, () => {
    console.log("Server listening: ", port)
})

Commands executed for the tests

ab -k -c 350 -n 10000 "http://127.0.0.1:1234/api?param1=12" // our custom web server
ab -k -c 350 -n 10000 "http://127.0.0.1:1235/api?param1=12" // the same express api

The above command executes 10000 requests with a concurrency up to 350.

Result for our custom web server:

Result for express:

Table of metrics

Results for experiment one

| Web server library | Time taken for the test | request per second (mean) | time per request (mean) | long request | | ------------------- | ----------------------- | ------------------------- | ----------------------- | ------------ | | mini-express-server | 3.525 sec | 2837.19 #/sec | 123.362 ms | 180 ms | | express | 4.137 sec | 2417.42 | 144.783 ms | 271 ms |

In addition, we modified the test suit, introducing a for loop to set 1000 routes in both web server implementations, and re-ran the test.

//////stressing the api//////////////
for (let i = 0; i < 1000; i++) {
    app.get(`/v1/endpoind/${i}`, (req, res) => {
        res.status(200).json({ "message": `Hello: ${i}` });
    })
}
//////////////////////////////////////

Results for experiment two

| Web server library | Time taken for the test | request per second (mean) | time per request (mean) | long request | | ------------------- | ----------------------- | ------------------------- | ----------------------- | ------------ | | mini-express-server | 3.488 sec | 2866.72 #/sec | 122.091 ms | 251 ms | | express | 6.304 sec | 1586.24 | 220.647 ms | 397 ms |

As you can appreciate in the results, the mini-express-server library beat the express library running the described test. Saving in the worsts case (where the API had more than 1000 endpoints) 100 ms in time per request, 146 ms in the long request, having half of the time for completing the test and increasing the capacity of requests per second to 1300 more than express.

Benchmarking with autocannon

In an example api configuration like:

// mini-express-server
const { AppServer } = require("mini-express-server");
const { generateRouteRandom } = require("../utils.js");

const app = new AppServer();
const port = 3000;

////////// Generate a lot of random routes ///////////////
for (let i = 0; i < 100; i++) {
    app.get(generateRouteRandom("api", 3), (req, res) => {
        return res.json({ hello: 'world' });
    })
}

for (let i = 0; i < 100; i++) {
    app.get(generateRouteRandom("home", 3), (req, res) => {
        return res.json({ hello: 'world' });
    })
}
//////////////////////////////////////////////////////////

app.get("/", (req, res) => {
    return res.json({ hello: 'world' });
})

app.listen(port, (address) => {
    console.log("Server created by library: mini-express-server is listening on port:", address)
})

Example for express

// express
const express = require("express");
const { generateRouteRandom } = require("../utils.js");

const app = express();
const port = 3000;

app.disable('etag')
app.disable('x-powered-by')

////////// Generate a lot of random routes ///////////////
for (let i = 0; i < 100; i++) {
    app.get(generateRouteRandom("api", 3), (req, res) => {
        return res.json({ hello: 'world' });
    })
}

for (let i = 0; i < 100; i++) {
    app.get(generateRouteRandom("home", 3), (req, res) => {
        return res.json({ hello: 'world' });
    })
}
//////////////////////////////////////////////////////////

app.get("/", (req, res) => {
    return res.json({ hello: 'world' });
})

app.listen(port, (address) => {
    console.log("Server created by library: express is listening on port:", address)
})
// koa
const Koa = require('koa')
const Router = require('koa-router');
const { generateRouteRandom } = require("../utils.js");
const app = new Koa()

const port = 3000;

////////// Generate a lot of random routes ///////////////
const routerBlok1 = new Router({ prefix: "/" });
const routerBlok2 = new Router({ prefix: "/" });
for (let i = 0; i < 100; i++) {
    routerBlok1.get(generateRouteRandom("api", 3), ctx => {
        ctx.body = { hello: 'world' }
    })
}

for (let i = 0; i < 100; i++) {
    routerBlok2.get(generateRouteRandom("home", 3), ctx => {
        ctx.body = { hello: 'world' }
    })
}
//////////////////////////////////////////////////////////
app.use(routerBlok1.routes());
app.use(routerBlok2.routes());

app.use(ctx => {
    ctx.body = { hello: 'world' }
})

app.listen(port, () => {
    console.log("Server created by library: koaa is listening on port:", port)
})

We perform the test using autocannon with the command:

autocannon -c 100 -d 10 -p 10 localhost:3000

And here the results:

mini-express-server

koa

express

Git Hub Examples

Here you can find more examples of using mini-express-server

  1. Basic CRUD Rest API, usage: middlewares like cors, body-parser, cookie-parser, ect.
  2. Basic server to upload and serving files.
  3. Serving html pages

AppServer

The main class for the AppServer that creates the HTTP server and routes requests to the appropriate handlers.

Properties

  • httpServer: An instance of the Node.js Server class or null or undefined.

  • port: The port number the server will listen to. Default is 8888.

  • mapGetHandlers: An instance of the RoutesTrie class for handling GET requests.

  • mapPostHandlers: An instance of the RoutesTrie class for handling POST requests.

  • mapPutHandlers: An instance of the RoutesTrie class for handling PUT requests.

  • mapDeleteHandlers: An instance of the RoutesTrie class for handling DELETE requests.

  • globalMiddlewares: An array of IMiddleware objects for global middlewares.

  • staticRouteMap: An object of type StaticRouteMap for storing the static route mapping.

  • customErrorHandler: A function for custom error handling.

Methods of AppServer Class

  • listen(port, cb?)

    Starts the HTTP server and listens for incoming requests on the specified port. The default port is 8888. A cb will be called once the server starts successfully

    Parameters

    • port: The port number the server will listen to.
    • cb: A callback function that will be called once the server starts successfully
  • get(route: string, route: string, ...cbs: IMiddleware[])

    Registers a route for handling GET requests.

    Parameters

    • route: The string representation of the route.
    • cbs: An array of functions or middleware functions for handling the GET request.
  • post(route: string, route: string, ...cbs: IMiddleware[])

    Registers a route for handling POST requests.

    • Parameters
      • route: The string representation of the route.
      • cbs: An array of functions or middleware functions for handling the POST request.
  • put(route: string, route: string, ...cbs: IMiddleware[])

    Registers a route for handling PUT requests.

    • Parameters

      • route: The string representation of the route.
      • cbs: An array of functions or middleware functions for handling the PUT request.
  • delete(route: string, route: string, ...cbs: IMiddleware[]) Registers a route for handling DELETE requests.

    • Parameters
      • route: The string representation of the route.
      • cbs: An array of functions or middleware functions for handling the DELETE request.
  • use(middleware: IMiddleware)

    Registers a middleware for all requests.

    • Parameters
      • middleware: An instance of the IMiddleware interface for the middleware.
  • use(route:String ,middleware: IMiddleware)

    Registers a middleware for a specific route.

    • Parameters
      • route: A path route.
      • middleware: An instance of the IMiddleware interface for the middleware.
  • setStatic(route: string, pathToStaticDir: string)

    Registers a static route for serving files.

    • Parameters
      • route: The string representation of the route.
      • pathToStaticDir: The path to the folder containing the files to be served.

Stackblitz example of basic API

https://stackblitz.com/edit/node-mpg9k4?file=index.js