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

@trenskow/app

v0.8.29

Published

A small HTTP router.

Downloads

117

Readme

@trenskow/app

A small HTTP router.

Introduction

This is a package for creating HTTP applications.

It is inspired by express – but uses modern JavaScript features and has a lot less features (on purpose) and has special emphasis on modularization of endpoints.

TOC

Usage

Example

Below is an example and the result of an application that uses the package.

This example complicates a route that could be vastly simplified. It just does this to show off some of the features of the package.

Code

/* index.js */

import { Application, Endpoint } from '@trenskow/app';

const app = new Application({ port: 8080 });

try {

	const root = new Endpoint()
		.mount('iam', await import('./iam.js'));

	const renderer = async ({ result, response }) => {
			response.headers.contentType = 'text/plain';
			response.end(result);
	};

	await app
		.root(root)
		.renderer(renderer)
		.open();

	console.info(`Application is running on port ${app.port}`)

} catch (error) {
	console.error(error);
}
/* iam.js */

import { Endpoint } from '@trenskow/app';

export default new Endpoint()
	.parameter({
		name: 'name',
		endpoint: await import('./name.js')
	});
/* greeter.js */

import { Router } from '@trenskow/app';

export default new Router()
	.use(async (context) => {
		context.greeter = (name) => `Hello, ${name}!`;
	})
/* name.js */

import { Endpoint } from '@trenskow/app';

export default new Endpoint()
	.middleware(await import('./greeter.js'))
	.get(async ({ parameters: { name }, greeter }) => greeter(name));

Result

The above example will handle a request like below.

GET /iam/trenskow HTTP/1.1
Host: localhost:8080
Accept: */*

– and will respond with below.

HTTP/1.1 200 OK
Content-Type: text/plain
Connection: keep-alive
Content-Length: 16

Hello, trenskow!

Designing your app

Since most people know express, it would make sense to point out some of the differences.

Routers

One key difference between express and this package is, that routes cannot define their own paths. Paths are defined by the parent endpoints "mounting" the child endpoint.

Router

The Router type is a basic router, which is only used when dealing with middleware (see below). It only supports one method, which is the .use(...) method.

Endpoint

Endpoint is the most used router, and it is also the one that is mostly similar to express' router. This is where you can define handlers for the HTTP methods – .get(...), .put(...), .delete(...), etc.

Unlike express, and as stated above, you cannot specify the path from a .get(...) method – you can only specify the handler, as the path is determined by the parent endpoint.

Endpoints has the mount method, which "mounts" a router to the specified subpath.

There is a variant of the mount method called parameter which is used to mount an endpoint with a dynamic path – whereas the path is treated like an input parameter (like express' .param method). Parameters also supports a transform function, which is able to transform the parameter into something else (eg. a user identifier into a user object).

Lastly there is the .middleware method, which is used to attach middleware. Middleware is defined as a router, which have the type Router and therefore cannot act as an endpoint. You can regard them like transforms or service providers for the request.

Endpoint extends Router.

Asynchronous everywhere!

All handlers and routers support async functions (and non-async). No need to call next, and when a method handler has a result available it just returns it – and if an error occur, you just throw an error. The provided renderer is responsible for writing the returned value to the response.

The context object

Where express gives you the (req, res, next) parameters for each handler, this application instead just provides a single parameter, the "context object", which contains all the information needed to process the request.

Middleware can assign values to the context to provide data and services, which is then available for subsequent endpoints, routers and handlers.

When a request is incoming, the context object looks like this.

| Name | Description | Type | | ---------------- | ------------------------------------------------------------ | :-------------------------: | | application | The application instance that has received the request. | Application | | request | The request object from the HTTP server. | Request | | response | The response object from the HTTP server. | Response | | parameters | An empty object that will contain the parameters picked up when processing the parameters (if any) of the requested path. | Object | | path | An object that has properties representing different paths. | Object | | path.full | An array of strings that joined represent the path of the fully requested path. | Array of String | | path.current | An array of strings that joined represents the path currently being processed. | Array of String | | path.remaining | An array of strings that joined represents the path that is above the currently processed path. Setting this will rewrite the remaining path (useful when serving single page applications to a browser). | Array of String | | query | An object holding the URL query parameters as an object (keys has been converted to camel case). | Object | | state | A string indicating the current state of the request – possible values are 'routing', 'rendering', 'completed' or 'aborted'. | String | | abort | A function that aborts the request. It takes the parameters (error, brutally), where error is the error that needs to be handled by the renderer – and brutally which indicates if the connection should also be closed. | AsyncFunction | | render | A function that tells the application to stop processing the request and jump directly to the renderer. | Function | | result | Whatever has been returned by the method handlers (should be written to the response in the renderer). | Any |

Example

Below is an example on how the context is used (also see the example in the beginning of this document).

.get(context) => { /* Use all the available information. */ }
.get({ parameters }) => { /* Use JavaScript object destructuring to get only the information you need. /* }

Casing

JavaScript is a camel cased language. HTTP is a mixture of different case types. Therefore this package converts all non-camel case identifiers to camel case for use when coding.

Converting between case type is performed by @trenskow/caseit.

HTTP headers

Case is automatically converted in both directions, so if you do context.response.headers.contentType = 'application/json' it will automatically be converted to Content-Type: application/json when the response is sent.

The same goes for request headers like Accept-Language: en which is accessible through context.request.headers.acceptLanguage.

Query parameters

Request with quuries like ?my-parameter=value is accessible through context.query.myParameter .

Mount paths

When match mode is set to 'loosely' (default) a request with the path component my-route or my_route will match an endpoint mounted at myRoute.

Endpoints, routers and handlers

This package distinguishes between endpoints, routers and handler.

Endpoints

Endpoints takes care of a path component. As example the /this/is/my/path/ path is handled by a specific endpoint. It only handles one explicit path, so as in the previous example, it does not handle /this/is/my/ – nor does it handle /this/is/my/path/endpoint/.

Endpoints can have a couple of things mounted / attached to it – those are.

When using endpoints

Whenever a function (such as .mount or .parameter or .root) takes an endpoint as a parameter, it can be provided in any of the following ways.

  • An instance of Endpoint.
  • An object that has a default key that has an instance of Endpoint as the value (useful when using inline imports as await import('my-endpoint.js')).
Routers

A router is the same as above, except it only supports .use.

When using routers

As above, whenever a function takes a router as a parameter, it can be provided in any of the following ways.

  • An instance of Router.
  • An object that has a default key that has an instance of Router as the value (useful when using inline imports as await import('my-route.js')).).
Handlers

Handlers are functions that handles a request. Handlers are functions that takes the context object as it's only parameter.

When using handlers

Whenever a function takes a handler as a parameter, it can be provided in any of the following ways.

  • A function that takes a context object as it's parameter.
    • (context) => { /* do whatever */ }
  • An object that has a default property that is set to the above.

API Reference

Application

The Application class holds an application and is responsible for handling and bootstrapping request from the server. It does not provide any routing on its own, instead it has a root method, which is used to set the root router.

If no root route has been set, all requests will be responded with 404 Not Found.

extends events.EventEmitter

Constructor

The Application class takes an "options" object as it's parameter.

Parameters

| Name | Description | Type | Required | Default value | | ------------------------ | ------------------------------------------------------------ | :-----------------------: | :------: | :--------------------------: | | options | An object representing the options. | Object | | {} | | options.port | The port at which to listen for incoming connections. | Number | | 0 (automatically assigned) | | options.RequestType | An object that inherits from the Request class (an http.IncomingMessage subclass) that is used as the request object in routes. | class | | Request | | options.ResponseType | An object that inherits from the Response class (http.ServerResponse subclass) that is used as the response object in routes. | class | | Response | | path | An object that represents path related options. | Object | | {} | | path.matchMode | Indicates how to match requests to mounted paths (eg. should the path be converted to camel case). | 'loosely' or 'strict' | | 'loosely' | | options.server | An object that represents how to instantiate the HTTP server. | Object | | {} | | options.server.create | A function that is able to create a server. | Function | | http.createServer | | options.server.options | An object to be passed as options when creating a server. | Object | | {} |

Events

opening

Indicates that the server is starting.

opened

Indicates the the server was started.

The listener callback will be passed the port number the server is listening on.

closing

Indicates that the server is being stopped.

closed

Indicates that the server has stopped.

Instance methods

open

This method opens (starts) the server. The server will accept connections using the port provided in the constructor.

Will throw an error if the state of the application is anything other than 'closed'.

Returns a Promise that resolves to the application.

Parameters

| Name | Description | Type | Required | Default value | | ------ | ----------------------------------------------------------------------------------- | :----: | :------: | :------------------------------------------------------: | | port | If no port was provided in the constructor it can be provided here. | Number | | Value set in constructor or 0 (automatically assigned) |

Parameters can be passed both as open(port) or open({ port }).

close

This method closes (stops) the server.

Will throw an error if the state of the application is anything other than 'open'.

Returns a Promise that resolves to the application.

Parameters

| Name | Description | Type | Required | Default value | | --------------------- | -------------------------------------------------------------- | :---: | :------: | :-----------: | | awaitAllConnections | Indicates not to return until all connections has been closed. | Bool | | false |

Parameters can be passed both as close(awaitAllConnections) or close({ awaitAllConnections }).

root

This method sets the root endpoint of the server.

The provided endpoint is the one that will handle all requests to /. It is where you set the "main entry" for your application.

If no root endpoint is provided the server will respond to all requests with 404 Not Found.

Returns the application.

Parameters

| Name | Description | Type | Required | Default value | | ---------- | -------------------------------------------------- | :---------------------: | :----------------: | :-----------: | | endpoint | The endpoint that will handle all requests to /. | Endpoint | :white_check_mark: | |

renderer

Sets the renderer function (async/non-async).

This method is responsible for writing to the response whatever the routes have returned (JSON encoding of values would be something to put in here).

Returns the application.

Parameters

| Name | Description | Type | Required | Default value | | ---------- | ----------------------------------------------------------- | :-----------------------: | :----------------: | :-------------------: | | renderer | A function that takes the context object as it's parameter. | Function or AsyncFunction | :white_check_mark: | See below |

Default

The default renderer will just write whatever is returned from the routes to the response. If it's a string it will set Content-Type: text/plain, if it's a buffer it will just write the buffer – otherwise it will just end the response.

Properties

port

Returns the port at which the server is currently listening.

server

Returns the underlying HTTP server instance.

state

Returns a string that represents the current state of the application.

Possible values

| Value | Description | | --------- | ------------------------------------------------------------- | | closed | The server is not listening for incoming connections. | | closing | The server is closing and waiting for clients to disconnect. | | open | The server is running and listening for incoming connections. | | opening | The server is currently in the process of opening. |

Endpoint

extends Router

An endpoint is what resembles the express.js router the most. It is the one where you define parameters and HTTP method handlers like GET, POST, PUT, DELETE, etc.

If an endpoint is requested with a HTTP method not implemented by the endpoint it will respond with 405 Method Not Allowed – otherwise if no HTTP methods has been implemented at all on the endpoint it will respond with 404 Not Found.

Constructor

The constructor takes no parameters.

Instance methods

get, post, put, delete, etc..

This method takes care of handing a specified HTTP method.

Supported HTTP methods are the same as those returned by http.METHODS.

You can only call these methods once per method per endpoint – calling it multiple times will result in only the last one getting used.

These also ends routing. After a method route has been called, the routing will go strait to the renderer.

Notice: If no head method is implemented on endpoint, get will instead be called (if ). When client requests a head the result will be ignored.

Returns the endpoint.

Parameters

| Name | Description | Type | Required | Default value | | ---------- | ------------------------------ | :------------------------------------------------------: | :----------------: | :-----------: | | handlers | A (or an array of) handlers. * | Function, AsyncFunction or Array (see also) | :white_check_mark: | |

* When more than one handler is provided only the return value of the last handler that returned a non-undefined value will be send the the renderer.

Example

Below is an example on how to use the method.

default export ({ endpoint }) => {  
	endpoint
		.get(
			async (context) => 'Hello, world!',
			() => console.info("Said hello."));
};

In the above example 'Hello, world!' is immediately send to the renderer and the request ends. The second handler is also executed, but as it returns undefined its return value is ignored.

Catch all

There is also a catch-all variant, which makes the handler able to handle the all paths from that endpoint (useful when serving files from a directory).

Below is an example.

import { Endpoint } from '@trenskow/app';

export default new Endpoint()
	.get.catchAll(async () => 'Hello, World!');
mount

The method mounts another endpoint to a specific subpath.

Returns the endpoint.

Parameters

| Name | Description | Type | Required | Default value | | ---------- | ----------------------------------------------------- | :-------------------------------: | :----------------: | :-----------: | | path | The path component the endpoint should be mounted to. | String (see also) | :white_check_mark: | | | endpoint | The endpoint to mount. | Endpoint | :white_check_mark: | |

Parameters can also be provided as { path, endpoint }.

Example

Below is an example on how to use the method.

import { Endpoint } from '@trenskow/app';

default export new Endpoint()

	.mount('pathComponent', { endpoint }) => {
		/* configure endpoint at `./path-component/` */
	})

	/* Below is an example of a shortcut method. */

	.mounts.pathComponent(({ endpoint }) => {
		/* configure endpoint at `./path-component/`. */
	});
parameter

This method mounts another endpoint, but uses the path as a dynamic value which is assigned to the context.parameter object.

Returns the endpoint.

Parameters

| Name | Description | Type | Required | Default value | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-----------------------: | :----------------: | :-----------: | | name | The key that is used when assigning to context.parameters. | String | :white_check_mark: | | | endpoint | The endpoint to mount. | Endpoint | :white_check_mark: | | | transform | A (async) function that can transform the value. It's first an only parameter is an object with { name /* name of parameter */, context }. If you're parameter is called user , the transform function will be called with an object { user, context }. | Function or AsyncFunction | | |

Parameters can also be provided as { name, endpoint, transform }.

Example

Below is an example on how to use the method.

import { Endpoint } from '@trenskow/app';

export default new Endpoint()

	.parameter('name',
		new Endpoint()
			.get(({ parameters: { name } }) => name))

	/* Below is an example of a shortcut method (also demonstrates transforms). */

	.parameters.user({
		transform: async ({ user }) => await getMyUserFromId(user),
		endpoint: new Endpoint()
			.get(({ parameters: { user } }) => `Hello ${user.name}!` })
	});
middleware

This method i attaches a piece of middleware. Middleware would typically be routes that do not handle an endpoint, but works as a transform or service provider (body parsers and rate limiters would typically be installed as middleware).

Returns the endpoint.

Parameters

| Name | Description | Type | Required | Default value | | -------- | ---------------------------------------- | :-----------------: | :----------------: | :-----------: | | router | The router that contains the middleware. | Router | :white_check_mark: | |

Example

Below is an example on how to implement a JSON body parser in a middleware router.

/* my-endpoint.js */

import { Endpoint } from '@trenskow/app';

export default = new Endpoint()
	.middleware(await import('./body-json-parser.js'))
	.post(({ body }) => JSON.stringify(body)); /* echo body to response */
/* body-json-parser.js */

import { Router, Error as AppError } from '@trenskow/app';

export default = new Router()
	.use(async (context) => {
	
		const { request } = context;
		const { headers } = request;

		const [
			contentType,
			charset = 'utf-8'
		] = headers.contentType?.match(/^application\/json(?:; ?charset=([a-z0-9-]+)(?:,|$))?/i);

		if (!/^application\/json$/i.test(contentType)) return;

		const chunks = [];

		try {
			for await (const chunk of request) {
				chunks.push(chunk);
			}
			context.body = JSON.parse(Buffer.concat(chunks).toString(charset));
		} catch (error) {
			throw new ApiError.BadRequest();
		}

	});
mixin

This method mixes in another endpoint into this.

Returns the endpoint.

Parameters

| Name | Description | Type | Required | Default value | | ---------- | -------------------------------------- | :-----------------------: | :----------------: | :-----------: | | endpoint | The endpoint to be mixed in into this. | Endpoint | :white_check_mark: | |

Example

Below is an example on how to use mixin.

/* endpoint-1.js */

import { Endpoint } from '@trenskow/app';

export default new Endpoint()
	.get(() => 'Hello, world!')
	.mixin(await import('./endpoint-2.js'));
/* endpoint-2.js */

export default new Endpoint()
	.post(async () => 'Hello, world from POST!');

Router

Constructor

The constructor takes no parameters.

Instance methods

use

This method is like the HTTP method handlers of Endpoint, except it is called with all HTTP methods and the return value is ignored. Routing continues after handler returns.

Typically used by middleware.

Returns the router.

Parameters

| Name | Description | Type | Required | Default value | | --------- | ------------------------- | :------------------------------------------------------: | :----------------: | :-----------: | | handler | A (or multiple) handlers. | Function, AsyncFunction or Array (see also) | :white_check_mark: | |

Example

See example above.

mixin

This method mixes in another router into this.

Returns the router.

Parameters

| Name | Description | Type | Required | Default value | | -------- | ------------------------------------ | :-----------------: | :----------------: | :-----------: | | router | The router to be mixed in into this. | Router | :white_check_mark: | |

Example

Below is an example on how to use mixin.

/* router-1.js */

import { Router } from '@trenskow/app';

export default new Router()
	.mixin(await import('./router-2.js'));
/* router-2.js */

import { Router } from '@trenskow/app';

export default new Router()
	.use(async () => {
		/* Your handler here */
	});
transform

This method hooks into the response chain and allows to change the result of the request. It will transform any value from below the routing tree from which it is added.

Parameterts

| Name | Description | Type | Required | Default value | | ----------- | --------------------------- | :-----------------------: | :----------------: | :-----------: | | transform | A (or multiple) transforms. | function | :white_check_mark: | |

Example

Below is an example of how to use a transform.

import { Endpoint } from '@trenskow/app';

export default new Endpoint()
	.transform(async ({ result, context }) => {
  	return (await result()) + ', World!';
	})
	.get(() => {
  	return 'Hello';
	});

When endpoint is called using HTTP GET, it will return 'Hello, World!'.

Request

This class represents the request. Each individual request has its own instance assigned to context.request, where it accessible from endpoint, routers and handlers.

extends http.IncomingMessage

Constructor

Request instances are constructed by the HTTP server and should not be initialized directly.

Instance properties

headers

Returns an object that has the request headers as key/values, where the keys has been converted to camel case.

Response

extends http.ServerResponse

Constructor

Response instances are constructed by the HTTP server and should not be initialized directly.

Events

writeHead

Indicates that the header was written to the client.

processed

Indicates that the response was processed.

The listener callback will be passed an Error object if an error occurred – or undefined if no error occurred.

Instance methods

getHeader

Returns the value of a header.

Parameters

| Name | Description | Type | Required | Default value | | ------ | ---------------------------------------------------------- | :----: | :----------------: | :-----------: | | name | The name of the header to return (camel cased). | String | :white_check_mark: | |

setHeader

Sets the value of a header.

Parameters

| Name | Description | Type | Required | Default value | | ------- | ------------------------------------------------------- | :----: | :----------------: | :-----------: | | name | The name of the header to set (camel cased). | String | :white_check_mark: | | | value | The value of the header. | String | :white_check_mark: | |

removeHeader

Removed a header.

Parameters

| Name | Description | Type | Required | Default value | | ------ | ---------------------------------------------------------- | :----: | :----------------: | :-----------: | | name | The name of the header to remove (camel cased). | String | :white_check_mark: | |

hasHeader

Return true if header has been set.

Parameters

| Name | Description | Type | Required | Default value | | ------ | --------------------------------------------------------- | :----: | :----------------: | :-----------: | | name | The name of the header to check (camel cased). | String | :white_check_mark: | |

getHeaderNames

Returns an array of all header names set.

Instance properties

headers

Returns an object that has the response headers as key/values, where the keys has been converted to camel case.

License

See license in LICENSE