Skip to content

Instantly share code, notes, and snippets.

@julianjensen
Created June 7, 2020 16:33
Show Gist options
  • Select an option

  • Save julianjensen/9b474d910032dd10f7e0c395d0c35484 to your computer and use it in GitHub Desktop.

Select an option

Save julianjensen/9b474d910032dd10f7e0c395d0c35484 to your computer and use it in GitHub Desktop.
A very basic HTTP server that supports standard middleware and express-style URLs.
/** ******************************************************************************************************************
* @file A very basic HTTP server that supports standard middleware and express-style URLs.
* @date 07-Jun-2020
*********************************************************************************************************************/
"use strict";
async function MinimalServer( { PROTOCOL = 'http', host = '127.0.0.1', port = 3000 } )
{
const protocol = await import( PROTOCOL );
const protoMethods = protocol.METHODS.map( m => m.toLowerCase() );
const methods = protoMethods.reduce( ( _, method ) => _.set( method, new Map() ), new Map() );
const middleware = [];
const events = {};
const toString = o => Object.prototype.toString.call( o );
const toRx = p => new RegExp( p.replace( /:([A-Za-z0-9_]+)(\?)?/g, ( $0, $name, $opt = '' ) => `(?<${$name}>[A-Za-z0-9_]+)${$opt}` ), 'gu' );
const emit = async ( event, payload ) => { ( events[ event ] || [] ).forEach( fn => fn( payload ) ); return payload; };
const addMethodHandler = method => ( url, fn ) => methods.get( method ).set( toRx( url ), promisify( fn ) );
const runMiddleware = ( req, res ) => middleware.reduce( ( prev, mware ) => prev.then( () => mware( req, res ) ), Promise.resolve() );
const error = res => ( msg, code = 500 ) => emit( 'error', msg ).then( () => res.writableEnded || res.writeHead( code ).end( typeof msg === 'string' ? msg : msg.message ) );
const promisify = handler => ( req, res ) => new Promise( ( resolve, reject ) => {
try
{
const p = handler( req, res, resolve );
if ( toString( p ) === '[object Promise]' )
p.then( resolve );
else if ( handler.length < 3 )
resolve( p );
}
catch ( err )
{
emit( 'error', err ).then( reject );
}
} );
const serverCode = ( req, res ) => {
const url = new URL( req.url, `${PROTOCOL}://${host}:${port}/` ),
_req = Object.create( req, { params: { enumerable: true, writable: true, value: {} } } );
runMiddleware( _req, res )
.then(
() => [ ...methods.get( req.method.toLowerCase() ) ].some( ( [ rxTest, fn ] ) =>
( _req.params = rxTest.exec( url.pathname ) ) &&
( _req.params = { ...( _req.params.groups || {} ), ...Object.fromEntries( url.searchParams.entries() ) } ) &&
fn( _req, res ).then( () => res.writableEnded || res.end() ) ) )
.catch( error( res ) );
};
protocol.createServer( serverCode ).listen( port, host, () => emit( 'log', `Server(${PROTOCOL}) running on ${host}:${port}` ) );
return Object.fromEntries( Object.entries( {
...protoMethods.reduce( ( _, method ) => ( {
..._,
[ method ]: addMethodHandler( method )
} ), {} ),
use: handler => middleware.push( promisify( handler ) ),
on: ( event, handler ) => ( events[ event ] || ( events[ event ] = [] ) ).push( handler )
} ).map( ([ name, fn ]) => [ name, function( ...args ) { fn( ...args ); return this; } ] ) );
}
/**
* Example usage. This example logs all requests made to the server, adds a signle `GET` endpoint, and logs messages and errors.
*/
(async () => {
const server = await MinimalServer( { PROTOCOL: 'http', host: '127.0.0.1', port: 3000 } );
server
.use( req => console.log( 'from req:', req.url ) )
.get( '/hello/:stuff', ( req, res ) => res.writeHead( 200, { 'Content-Type': 'text/html' } ).write( '<table><tr><th>Name</th><th>Value</th></tr>' +
[ ...Object.entries( req.params ) ].map(([ name, value]) => `<tr><td>${name}</td><td>${value}</td></tr>` ).join('') + '</table>' ) )
.on( 'log', x => console.log( 'logged:', x ) )
.on( 'error', x => console.error( 'error:', x ) );
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment