@trenskow/app

0.8.29 • Public • Published

@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
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 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)

* 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)
endpoint The endpoint to mount. Endpoint

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
endpoint The endpoint to mount. Endpoint
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
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
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)
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
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
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
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
value The value of the header. String
removeHeader

Removed a header.

Parameters
Name Description Type Required Default value
name The name of the header to remove (camel cased). String
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
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

Package Sidebar

Install

npm i @trenskow/app

Weekly Downloads

13

Version

0.8.29

License

BSD-2-Clause

Unpacked Size

68.8 kB

Total Files

15

Last publish

Collaborators

  • trenskow