Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/inpassor/ts-node-server
Simple node.js HTTP / HTTPS server
https://github.com/inpassor/ts-node-server
http https node server simple
Last synced: 3 days ago
JSON representation
Simple node.js HTTP / HTTPS server
- Host: GitHub
- URL: https://github.com/inpassor/ts-node-server
- Owner: Inpassor
- License: mit
- Created: 2020-02-01T10:51:30.000Z (almost 5 years ago)
- Default Branch: master
- Last Pushed: 2023-07-18T20:46:52.000Z (over 1 year ago)
- Last Synced: 2024-10-05T22:17:55.229Z (about 1 month ago)
- Topics: http, https, node, server, simple
- Language: TypeScript
- Size: 618 KB
- Stars: 3
- Watchers: 1
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Simple node.js HTTP / HTTPS server
[![](https://img.shields.io/npm/v/@inpassor/node-server.svg?style=flat)](https://www.npmjs.com/package/@inpassor/node-server)
[![](https://img.shields.io/github/license/Inpassor/ts-node-server.svg?style=flat-square)](https://github.com/Inpassor/ts-node-server/blob/master/LICENSE)
![](https://img.shields.io/npm/dt/@inpassor/node-server.svg?style=flat-square)A simple HTTP / HTTPS server written in pure node.js without Express.
This library is designed to be minimalistic, quick and powerful at once.
It includes two built-in middleware functions: **staticHandler** and **routeHandler**.
They act one after another. First, **staticHandler**, then **routeHandler**.
You can implement any number of middleware functions. They will be served first.**staticHandler** serves static files under a public directory.
It looks for directories/files by URI. If URI matches an existing directory,
**staticHandler** looks for an index file within it.
The library determines a few MIME types by a file extension.
You can define additional MIME types in the **Server** config.If URI does not match any public directory/file the Server runs **routeHandler**.
**routeHandler** serves routes and runs user-implemented Components.
You can implement any REST method within Component.
Routes are defined in the **Server** config.## Installation
```bash
npm install @inpassor/node-server --save
```## Usage
### Server [class]
`constructor(config?: ServerConfig)`
Creates a **Server** instance with a given config.
If config is omitted default options are used.**ServerConfig** is an object with any set of these options:
- `protocol: 'http' | 'https'` (default: **'http'**) - a protocol to be used by a server.
Depending on it a corresponding server instance will be created:
**HttpServer** or **HttpsServer**.
- `port: number` (default: **80**) - a port to be used by a server.
- `options: HttpServerOptions | HttpsServerOptions` (default: **{}**) - options to pass
to **createServer** function.
- `publicPath: string | string[]` (default: **'public'**) - a directory to be served by
**staticHandler**. Automatically resolves by **path.resolve** function.
- `index: string` (default: **'index.html'**) - an index file name to be served by
**staticHandler**. If URI matches an existing directory
under **publicPath** directory, **staticHandler** is looking for this file
within this directory and renders it by a corresponding renderer.
A renderer is determined by an index file extension.
- `mimeTypes: { [extension: string]: string }` (default: **{}**) - additional
MIME types by extension. For example:
```
{
mp3: 'audio/mpeg',
pdf: 'application/pdf',
doc: 'application/msword',
}
```
- `headers: { [name: string]: string }` (default: **{}**) - list of headers for
all the server responses. For example:
```
{
'Access-Control-Allow-Methods': 'OPTIONS, GET, POST',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers': 'content-type, authorization',
}
```
- `sameOrigin: boolean` (default: **false**) - when set to true adds headers
**Access-Control-Allow-Origin** equal to request **Origin** header
and **Vary** equal to 'Origin' to all the server responses.
If a request does not contain **Origin** header, headers **Access-Control-Allow-Origin**
and **Vary** are not added.
- `handlers: Handler[]` (default: **[]**) - additional middleware functions.**Handler** is a function
`(request: Request, response: Response, next: () => void): void`.It accepts three arguments:
- `request: Request` - **Request** instance.
- `response: Response` - **Response** instance.
- `next: () => void` - A function that passes control to a next middleware function
if is called inside a handler function.You can also call **Server.use** method to add middleware after **Server** instance
created.- `routes: Route[]` (default: **[]**) - routes to be served by **routeHandler**.
**Route** is an object:
```
{
path: string;
component: typeof Component;
headers?: { [name: string]: string };
}
```- `path: string` - A path pattern. You can specify named path parameters
by enclosing parameters names in `<...>`.For example: the path pattern `'shop//'` matches the route
`shop/audio/speakers-101`. In this case there will be two path parameters:```typescript
{
category: 'audio',
item: 'speakers-101',
}
```By default value of named path parameter can be any set of these symbols:
**a-zA-Z0-9-\_**.You can specify path parameter type by adding to its name **'|n'** (for numbers)
or **'|l'** (for letters).Example:
`` - named parameter "id" expected to consist of numbers (**0-9**);
`` - named parameter "category" expected to consist of latin letters (**a-zA-Z**).
A last named path parameter can be non-obligatory. In this case, we need
to "hide" the last slash inside the name and add **|?**: ``.
For example: `'shop/'`You can also specify a type of a non-obligatory last named path parameter: ``
or ``You can use "magic" path: **'\*'**, which matches any route.
A **Route** with this path should be defined after all the routes.- `component: typeof Component` - A derivative class of **Component**.
- `headers: { [name: string]: string }` (default: **{}**) - Additional headers
for this route.- `renderers: { [extension: string]: Renderer }` (default: **{}**) - list of
render functions by extension. For example:```
{
ejs: ejsRender, // don't forget to import { render as ejsRender } from 'ejs';
}
```**Renderer** is a function
`(template: string, params?: Params) => string`.It accepts one or two arguments:
- `template: string` - A template string.
- `params: Params` (default: **undefined**) - An object containing
data to be used by a render function.Returns string - a result of render function to be sent to a client.
- `bodyParsers: { [mimeType: string]: BodyParser }`
(default: **{ 'application/json': JSON.parse }**) - list of parser functions
by MIME type.**BodyParser** is a function
`(body: string) => string`It accepts one argument:
- `body: string` - Request body.
Returns parsed body and stores it to **Request.body**.
- `maxBodySize: number` (default: **2097152** - 2Mb) - maximum size of Request body.
#### Server.run [method]
`run: () => HttpServer | HttpsServer`
Runs an HttpServer | HttpsServer and returns its instance.
#### Server.handle [method]
`handle: (request: Request, response: Response) => void`
A Server handler. Called by **Server.run** method automatically.
It can be used by an external server (for example, Firebase Cloud functions).#### Server.use [method]
`use: (handler: Handler) => void`
Adds a middleware to a **Server** instance.
### Component [class]
All the user-implemented Component classes for **routeHandler** should be derivative of
**Component** class.You can implement any REST method within a Component class. All you need to do is
create a method of a class with a name coinciding with a request method name (in lower case).
Create **all** method to serve all the request methods.For example, this is DemoComponent implementing GET and POST methods:
```typescript
class DemoComponent extends Component {
public get(): void {
this.response.renderFile([__dirname, 'demo-component.ejs'], {
title: 'Demo Component',
});
}public post(): void {
this.response.send(200, 'This is the DemoComponent POST action');
}
}
```#### Component.app [property]
`app: Server`
#### Component.request [property]
`request: Request`
#### Component.response [property]
`response: Response`
### Request [class]
A derivative class of **IncomingMessage**. Has a few additional properties:
#### Request.app [property]
`app: Server`
#### Request.uri [property]
`uri: string`
Current route URI.
#### Request.params [property]
`params: { [name: string]: string }`
A named route parameters list.
#### Request.searchParams [property]
`searchParams: URLSearchParams`
#### Request.body [property]
`body: string`
A body of the request. Is parsed by **BodyParser** function if **bodyParsers** config
option contain key equal to **Content-Type** request header.### Response [class]
A derivative class of **ServerResponse**. Has a few additional properties and methods:
#### Response.app [property]
`app: Server`
#### Response.request [property]
`request: Request`
#### Response.send [method]
`send: (status: number, body?) => void`
Sends a response to a client.
Accepts one or two arguments:
- `status: number` - HTTP status code.
- `body: string` (default: **undefined**) - A body of response.#### Response.sendJSON [method]
`sendJSON: (data: string) => void`
Sends a response to a client in JSON format.
Accepts one argument:
- `data: string` - A data to be sent in JSON format in a body of response.
#### Response.sendError [method]
`sendError: (error) => void`
Sends an error response to a client.
Accepts one argument:
- `error: unknown` - Error object. The library tries to get HTTP status code
and error message automatically. Basically, error object should be as follows:
```
{
code: number;
message: string;
}
```#### Response.render [method]
`render: (template: string | Buffer, extension: string, params?: Params) => void`
Renders a **template** by a renderer, determined by **extension**, and responds to a client
with a body, containing a result of a render function.Accepts two or three arguments:
- `template: string | Buffer` - A template **string** or **Buffer**.
If a template is of **Buffer** type, it's converted to **string**.
- `extension: string` - A renderer will be determined by this **extension**.
For example, **'ejs'** will be rendered by EJS renderer.
- `params: Params` (default: **undefined**) - An object containing
data to be used by a render function.#### Response.renderFile [method]
`renderFile: (pathSegments: string | string[], params?: Params) => void`
Renders a file by a renderer, determined by a file extension, and responds to a client
with a body, containing a result of a render function.Accepts one or two arguments:
- `pathSegments: string | string[]` - A file name to be rendered.
Automatically resolves by **path.resolve** function.
- `params: Params` (default: **undefined**) - An object containing
data to be used by a render function.### Helpers
The library has a few helper functions:
#### formatBytes [function]
`formatBytes: (bytes: number, decimals = 2) => string`
#### getCodeFromError [function]
`getCodeFromError: (error) => number`
#### getMessageFromError [function]
`getMessageFromError: (error) => string`
#### resolvePath [function]
`resolvePath: (...pathSegments) => string`
#### httpStatusList [object]
`httpStatusList: { [code: number]: string }`
#### mimeTypes [object]
`mimeTypes: { [extension: string]: string }`
#### isHttpServerOptions [type guard]
`isHttpServerOptions: (arg) => arg is ServerOptions`
#### isHttpsServerOptions [type guard]
`isHttpsServerOptions: (arg) => arg is ServerOptions`
#### isServerConfig [type guard]
`isServerConfig: (arg) => arg is ServerConfig`
#### Logger [class]
## Examples
### Stand-alone
```typescript
import { Server, Component, ServerConfig } from '@inpassor/node-server';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { render as ejsRender } from 'ejs';class ErrorComponent extends Component {
public all(): void {
this.response.sendError({
code: 405,
});
}
}class DemoComponent extends Component {
public get(): void {
console.log(this.request.params);
this.response.renderFile([__dirname, 'demo-component.ejs'], {
title: 'Demo Component',
});
}public post(): void {
console.log(this.request.params);
this.response.send(200, 'This is the DemoComponent POST action');
}
}const config: ServerConfig = {
protocol: 'https', // 'http|https', default: 'http'
port: 8080, // default: 80
options: {
// ServerOptions for HTTP or HTTPS node.js function createServer, default: {}
key: readFileSync(resolve(__dirname, 'certificate.key.pem')),
cert: readFileSync(resolve(__dirname, 'certificate.crt.pem')),
ca: readFileSync(resolve(__dirname, 'certificate.fullchain.pem')),
},
publicPath: 'public', // path to public files, default: 'public'
index: 'index.html', // index file name, default: 'index.html'
mimeTypes: {
// additional MIME types
mp3: 'audio/mpeg',
pdf: 'application/pdf',
doc: 'application/msword',
},
headers: {
// list of headers for all the server responses, default: {}
'Access-Control-Allow-Methods': 'OPTIONS, GET, POST',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers': 'content-type, authorization',
},
sameOrigin: true, // when set to true adds headers 'Access-Control-Allow-Origin' equal to
// request Origin header and 'Vary' equal to 'Origin' to all the server responses
handlers: [], // additional middleware functions
// (you can also call Server.use method to add middleware after Server instance created)
routes: [
// routes to be served by routeHandler
{
path: 'demo',
component: DemoComponent,
},
{
path: '*',
component: ErrorComponent,
},
],
renderers: {
// list of render functions
ejs: ejsRender,
},
};const server = new Server(config);
// Add middleware
server.use((request, response, next) => {
// TODO: some middleware work// call next function to pass work to next middleware
// next();// or send a response to a client, otherwise, the server will hang till timeout
// use Response.send method in order to send all the needed headers defined in the config
response.send(200, 'Some content');
});server.run();
```We had created a Server instance with ejs renderer and two components:
DemoComponent, having GET and POST methods,
and ErrorComponent, serving all the routes (which did not match any previous route)
and all the request methods.The route **/demo[/arg]** will be served by DemoComponent.
All the other routes will be served first under **publicPath** directory, then
ErrorComponent will act.### socket.io
```typescript
import { Server, ServerConfig } from '@inpassor/node-server';
import * as socketIO from 'socket.io';const config: ServerConfig = {}; // define your own ServerConfig here
const server = new Server(config);
const serverInstance = server.run(); // instance of HTTP or HTTPS node.js Server
const io = socketIO(serverInstance, {
handlePreflightRequest: (request, response) => {
response.writeHead(204, {
'Access-Control-Allow-Methods': 'OPTIONS, GET',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers': 'content-type, authorization',
'Access-Control-Allow-Origin': request.headers.origin,
Vary: 'Origin',
});
response.end();
},
});
```### Firebase Cloud functions
There is no need for HTTP or HTTPS node.js server instance since Firebase Cloud functions
create its own server. We just need to pass **Server.handle** method to Firebase.#### Common usage
```typescript
import { RuntimeOptions, HttpsFunction, runWith } from 'firebase-functions';
import { Server, ServerConfig } from '@inpassor/node-server';const firebaseApplication = (
config: ServerConfig,
runtimeOptions?: RuntimeOptions
): HttpsFunction => {
const server = new Server(config);
return runWith(runtimeOptions).https.onRequest(server.handle.bind(server));
};const config: ServerConfig = {}; // define your own ServerConfig here
export const firebaseFunction = firebaseApplication(config, {
timeoutSeconds: 10,
memory: '128MB',
});
```#### Asynchronous Server config
```typescript
import { RuntimeOptions, HttpsFunction, runWith } from 'firebase-functions';
import { Server, ServerConfig } from '@inpassor/node-server';const firebaseApplication = (
getConfig: ServerConfig | Promise,
runtimeOptions?: RuntimeOptions
): HttpsFunction => {
return runWith(runtimeOptions).https.onRequest(async (request, response) => {
await new Promise((resolve, reject) => {
Promise.resolve(getConfig).then(
(config): void => {
const server = new Server(config);
resolve(server.handle.call(server, request, response));
},
(error) => reject(error)
);
});
});
};// Some asynchronous get config function
const getConfig = (): Promise => {
const config: ServerConfig = {}; // define your own ServerConfig here
return Promise.resolve(config);
};export const firebaseFunction = firebaseApplication(getConfig(), {
timeoutSeconds: 10,
memory: '128MB',
});
```You can also use the library
[@inpassor/firebase-application](https://github.com/Inpassor/ts-firebase-application)
which wraps node-server.