UNPKG

@node-scarlet/http

Version:

Build APIs Instantly 🏋️‍♂️

213 lines (206 loc) 5.61 kB
import * as http from 'http'; import * as qs from 'querystring'; import * as UrlPattern from 'url-pattern'; import { promisify } from 'util'; import { Readable } from 'stream'; export const server = () => new HttpServer(); export const response = (options:any={}) => new HttpResponse(options); class HttpServer { private _server:http.Server; listen; close; port; middlewares; route; constructor() { this.listen = listen.bind(this) this.close = close.bind(this) this.port = port.bind(this) this.route = route.bind(this); this.middlewares = { GET: [], PUT: [], POST: [], PATCH: [], DELETE: [], } } } /** * Create an Http Server and start listening for requests * Bound as a method to `HttpServer` */ function listen(port:number=0) { this._server = http.createServer(async (req:http.IncomingMessage, res:http.ServerResponse) => { try { // Separate data const { url, method, headers } = req; const [path, querystring] = url.split('?'); let body:any = await asyncBody(req); // Parse Querystring const query = Object.assign({}, qs.decode(querystring)); // Try to parse JSON data if (headers['content-type'] == 'application/json') { try { body = JSON.parse(body) } catch (e) {} } // Try to parse URL-Encoded data else if (headers['content-type'] == 'application/x-www-form-urlencoded') { body = Object.assign({}, qs.decode(body)); } // Adapt request const meta = {}; const request:any = { url, method, query, headers, body, } // See if any middleware matches the incoming request for (const m of this.middlewares[method]) { let match; try { // Some urlpatterns will throw errors match = m.match(path); } catch (e) { continue } if (match) { request.params = match; const response = await m.handler(request, meta); if (response) { const adaptable = responseShorthand(response); return adaptResponse(adaptable, res); } } } // Not Found const notFoundResponse = response({ status: 404 }) adaptResponse(notFoundResponse, res); } catch (e) { throw e } }); this._server.listen(port); } /** * Stop listening for http requests * Bound as a method to `HttpServer` */ function close() { this._server.close(); } /** * Determine the port that's being listened over * Bound as a method to `HttpServer` */ function port() { return this._server.address().port; } /** * Adapts `HttpResponse` it to its lower level counterpart`http.ServerResponse`. * A helper function of `HttpServer.listen()` */ function adaptResponse( input:HttpResponse, output:http.ServerResponse ) { const { status, headers, body } = input; if (!(body instanceof Readable)) headers['Content-Length'] = Buffer.byteLength(body); output.writeHead(status, { ...headers }) if (body instanceof Readable) { body.pipe(output); } else { output.end(body); } } /** * Get stringified request body asyncronously * @param {http.IncomingMessage} req * @returns {Promise<string>} */ const asyncBody = promisify(function(req:http.IncomingMessage, callback) { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { callback(null, body); }); req.on('error', (e) => { callback(e); }) }); /** * Convert primitive response types into intances of `HttpResponse` * A helper function of `HttpServer.start()` * A helper function of `HttpServer.listen()` */ function responseShorthand(response:any):HttpResponse { if (typeof response == 'string') { return { status: 200, headers: { 'content-type': 'text/html' }, body: response } } else if (typeof response == 'number' && statusMessages[response]) { return { status: response, headers: { 'content-type': 'text/plain' }, body: statusMessages[response] } } else if (response instanceof Readable) { return { status: 200, body: response } } else if (typeof response == 'object' && !(response instanceof HttpResponse)) { return { status: 200, headers: { 'content-type': 'application/json' }, body: JSON.stringify(response), } } else return response; } class Middleware { method: 'string'; urlpattern: 'string'; handler: (req, meta) => any; match: (url) => { [key:string]: string }; constructor(method, urlpattern, handler) { this.method = method; this.urlpattern = urlpattern; this.handler = handler; this.match = url => new UrlPattern(urlpattern).match(url); } } function route(method, urlpattern, handler) { method = method.toUpperCase(); if (!methods[method]) throw new Error(`Unsupported verb "${method}".`); this.middlewares[method].push(new Middleware(method, urlpattern, handler)); } class HttpResponse { status?: number; headers?: object; body?: string | Readable; constructor(options) { const { status, headers, body } = options; this.status = status ? status : 200; this.headers = headers ? headers: {}; this.body = body != undefined ? body: ''; } } const statusMessages = { 200: 'OK', 201: 'Created', 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', 409: 'Conflict', 500: 'Internal Server Error', } export const methods = { GET: 'GET', PUT: 'PUT', POST: 'POST', DELETE: 'DELETE' }