0.8.29 • Published 2 months ago

@trenskow/app v0.8.29

Weekly downloads
-
License
BSD-2-Clause
Repository
github
Last release
2 months ago

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

NameDescriptionType
applicationThe application instance that has received the request.Application
requestThe request object from the HTTP server.Request
responseThe response object from the HTTP server.Response
parametersAn empty object that will contain the parameters picked up when processing the parameters (if any) of the requested path.Object
pathAn object that has properties representing different paths.Object
path.fullAn array of strings that joined represent the path of the fully requested path.Array of String
path.currentAn array of strings that joined represents the path currently being processed.Array of String
path.remainingAn 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
queryAn object holding the URL query parameters as an object (keys has been converted to camel case).Object
stateA string indicating the current state of the request – possible values are 'routing', 'rendering', 'completed' or 'aborted'.String
abortA 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
renderA function that tells the application to stop processing the request and jump directly to the renderer.Function
resultWhatever 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
NameDescriptionTypeRequiredDefault value
optionsAn object representing the options.Object{}
options.portThe port at which to listen for incoming connections.Number0 (automatically assigned)
options.RequestTypeAn object that inherits from the Request class (an http.IncomingMessage subclass) that is used as the request object in routes.classRequest
options.ResponseTypeAn object that inherits from the Response class (http.ServerResponse subclass) that is used as the response object in routes.classResponse
pathAn object that represents path related options.Object{}
path.matchModeIndicates how to match requests to mounted paths (eg. should the path be converted to camel case).'loosely' or 'strict''loosely'
options.serverAn object that represents how to instantiate the HTTP server.Object{}
options.server.createA function that is able to create a server.Functionhttp.createServer
options.server.optionsAn 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
NameDescriptionTypeRequiredDefault value
portIf no port was provided in the constructor it can be provided here.NumberValue 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
NameDescriptionTypeRequiredDefault value
awaitAllConnectionsIndicates not to return until all connections has been closed.Boolfalse

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
NameDescriptionTypeRequiredDefault value
endpointThe 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
NameDescriptionTypeRequiredDefault value
rendererA 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
ValueDescription
closedThe server is not listening for incoming connections.
closingThe server is closing and waiting for clients to disconnect.
openThe server is running and listening for incoming connections.
openingThe 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
NameDescriptionTypeRequiredDefault value
handlersA (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
NameDescriptionTypeRequiredDefault value
pathThe path component the endpoint should be mounted to.String (see also):white_check_mark:
endpointThe 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
NameDescriptionTypeRequiredDefault value
nameThe key that is used when assigning to context.parameters.String:white_check_mark:
endpointThe endpoint to mount.Endpoint:white_check_mark:
transformA (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
NameDescriptionTypeRequiredDefault value
routerThe 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
NameDescriptionTypeRequiredDefault value
endpointThe 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
NameDescriptionTypeRequiredDefault value
handlerA (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
NameDescriptionTypeRequiredDefault value
routerThe 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
NameDescriptionTypeRequiredDefault value
transformA (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
NameDescriptionTypeRequiredDefault value
nameThe name of the header to return (camel cased).String:white_check_mark:
setHeader

Sets the value of a header.

Parameters
NameDescriptionTypeRequiredDefault value
nameThe name of the header to set (camel cased).String:white_check_mark:
valueThe value of the header.String:white_check_mark:
removeHeader

Removed a header.

Parameters
NameDescriptionTypeRequiredDefault value
nameThe name of the header to remove (camel cased).String:white_check_mark:
hasHeader

Return true if header has been set.

Parameters
NameDescriptionTypeRequiredDefault value
nameThe 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

0.8.29

2 months ago

0.8.27

2 months ago

0.8.26

2 months ago

0.8.28

2 months ago

0.8.23

2 months ago

0.8.25

2 months ago

0.8.24

2 months ago

0.8.22

4 months ago

0.8.21

4 months ago

0.8.20

4 months ago

0.8.9

9 months ago

0.8.8

9 months ago

0.8.5

9 months ago

0.8.4

10 months ago

0.8.7

9 months ago

0.8.6

9 months ago

0.7.11

10 months ago

0.7.10

10 months ago

0.7.9

10 months ago

0.7.12

10 months ago

0.7.8

10 months ago

0.8.12

8 months ago

0.8.11

8 months ago

0.8.14

7 months ago

0.8.13

8 months ago

0.8.10

9 months ago

0.8.19

7 months ago

0.8.16

7 months ago

0.8.15

7 months ago

0.8.18

7 months ago

0.8.17

7 months ago

0.8.1

10 months ago

0.8.3

10 months ago

0.8.2

10 months ago

0.7.6

1 year ago

0.7.5

1 year ago

0.7.7

1 year ago

0.7.2

2 years ago

0.7.1

2 years ago

0.7.4

2 years ago

0.7.3

2 years ago

0.7.0

2 years ago

0.6.7

2 years ago

0.6.9

2 years ago

0.6.8

2 years ago

0.6.10

2 years ago

0.6.12

2 years ago

0.6.11

2 years ago

0.6.14

2 years ago

0.6.13

2 years ago

0.6.6

2 years ago

0.5.14

2 years ago

0.5.13

2 years ago

0.6.3

2 years ago

0.6.2

2 years ago

0.6.5

2 years ago

0.6.4

2 years ago

0.6.1

2 years ago

0.6.0

2 years ago

0.5.12

2 years ago

0.5.11

2 years ago

0.5.10

2 years ago

0.5.9

2 years ago

0.5.8

2 years ago

0.5.7

2 years ago

0.5.6

2 years ago

0.5.5

2 years ago

0.5.4

2 years ago

0.5.3

2 years ago

0.5.2

2 years ago

0.5.1

2 years ago

0.5.0

2 years ago

0.4.6

2 years ago

0.4.5

2 years ago

0.4.4

2 years ago

0.4.3

2 years ago

0.4.1

2 years ago

0.4.0

2 years ago

0.3.1

2 years ago

0.3.0

2 years ago

0.2.4

2 years ago

0.2.3

2 years ago

0.2.2

2 years ago

0.2.1

2 years ago

0.2.0

2 years ago

0.1.2

2 years ago

0.1.1

2 years ago

0.1.0

2 years ago