@peter-schweitzer/ezserver
Version:
BLAZINGLY FAST npm module for backend/REST-API development with 0! dependencies, inspired by express
250 lines (200 loc) • 9.18 kB
JavaScript
/**
** TOC:
* - setup .................... #L15
* - endpoints ................ #L25
* - without parameters ..... #L31
* - with parameters ........ #L69
* - with wildcard .......... #L85
* - middleware ............... #L98
* - general middleware ..... #L106
* - specific middleware .... #L114
* - types .................... #L126
* - utility functions ........ #L177
*/
/**
** ================ Setup ===================
*/
import { LOG } from '@peter-schweitzer/ez-utils';
import { App, buildRes, getBodyJSON, MIME, serveFromFS, throw404 } from '../index.js';
const app = new App();
app.listen(65535);
/**
** ================ Endpoints ===================
*/
// An Endpoint resolves the specified URI.
/* ================ without param =================== */
// Endpoints without parameters have the highest specificity, so they get matched first
// these are the most specific, as they only resolve a specific URI, requested via a specific method (also supports head, connect and trace)
app.get('/get', (_req, res, _params) => {
buildRes(res, 'get');
});
app.put('/put', (_req, res, _params) => {
buildRes(res, 'put');
});
app.post('/post', (_req, res, _params) => {
buildRes(res, 'post');
});
app.delete('/delete', (_req, res, _params) => {
buildRes(res, 'delete');
});
app.patch('/patch', (_req, res, _params) => {
buildRes(res, 'patch');
});
app.options('/options', (_req, res, _params) => {
buildRes(res, 'options');
});
// add() ignores the method, hence it's less specific than the examples above
app.add('/', (_req, res, _params) => {
serveFromFS(res, './html/home.html');
});
app.add('/favicon.ico', (_req, res, _params) => {
buildRes(res, null, { code: 404 });
});
/* ================ with parameters =================== */
//params are prefixed with ':' and can be registered with all above mentioned functions
// endpoints with parameters are less specific than endpoints without
// for more info on the Params class look at the types-section
// going to '/echo-route-params/first-param/second-param' will return 'first-param second-param'
app.add('/echo-route-params/:param1/:param2', (_req, res, params) => {
buildRes(res, `${params.route('param1')} ${params.route('param2')}`);
});
// going to '/sum/351/69' will return '420'
app.add('/sum/:a/:b', (_req, res, params) => {
buildRes(res, `${params.routeNumber('a', 0) + params.routeNumber('b', 0)}`);
});
/* ================ with wildcard =================== */
// by having '/:*' as the last part of the URI the endpoint will resolve all requests that start with the URI
// params will include the rest of the requested URI in route
// params.route('*') will return an array of strings (the rest split on every '/')
// going to '/wildcard/a/b/c' will return '3 (a-b-c)'
// going to '/wildcard' will return '0 ()'
app.add('/wildcard/:*', (_req, res, params) => {
const rest = params.route('*');
buildRes(res, `${rest.length} (${rest.join('-')})`);
});
/**
** ================ middleware ===================
*/
// Middleware gets called in order of adding them
// Middleware gets passed the underlying 'query' and 'route' LUT
// When the request gets resolved (i.e. by calling buildRes()) within middleware the rest of the middleware stack and the ResFunction (or 404 function) won't get called
/* ================ general middleware =================== */
// To use middleware that gets called on every request call app.use() with a Middleware
// Calling app.use() also returns app so you can chain .use() calls and/or register middleware directly after instantiating
// you can see the below debug output in the terminal on every request
app.use({ handle: async (req, res, query, route) => LOG(`handeling request for '${req.url}', with method '${req.method}'`) });
/* ================ specific middleware =================== */
// By calling use() on MiddlewareCurry you can add middleware to a specific ResFunction
// going to '/middleware/echo' will return "method: 'GET', url: '/middleware/echo', uri: '/middleware/echo'"
app
.add('/middleware/echo', (req, res, params) => buildRes(res, 'this should not be seen'))
.use({ handle: async (req, res, query, route) => buildRes(res, `method: '${req.method}', url: '${req.url}', uri: '${req.uri}', query: '${JSON.stringify(query)}'`) });
// going to '/middleware/error' will return 'middleware error'
app.add('/middleware/error', (req, res, params) => buildRes(res, 'this should not be seen')).use({ handle: async (req, res, query, params) => 'middleware error' });
/**
** ================ Types ===================
*/
/**
** MIME
* MIME is an Object that provides nice auto completion for the most commonly used mime types.
* Instead of writing them out every time you can just use STD_MIME and auto completion.
* This aim is to:
* - decreases the amount of magic strings in your code
* - avoid errors tue to miss typing
* - spend less time repetitively typing the same few strings over and over
*/
/**
** Methods
* functions that expect a HTTP method use this type
* it is a list of all accepted strings i.e. {"GET"|"HEAD"|"POST"|"PUT"|"DELETE"|"CONNECT"|"OPTIONS"|"TRACE"|"PATCH"}
*/
/**
** ErrorOr<T>
* the ErrorOr type is used to propagate errors in a predictable and consistent way
* it has two properties: 'err' and 'data'
* if an error occurs 'err' is a string and 'data' null, otherwise 'err' is null and data 'T' ('T' is a template and can be any type)
*/
/**
** Params
* every resFunction gets passed a params object as the third argument
* params include all query parameters, as well as route parameters when specified
* route parameters are a component of the route and prefixed with ':' (e.g. '/:' signifies a route parameter)
*/
// going to '/echo-msg?msg=hello&msg2=world' will return 'hello world'
app.add('/echo-msg', (_req, res, params) => {
buildRes(res, `${params.query('msg', '')} ${params.query('msg2', '')}`);
});
/**
** EZIncomingMessage
* EZIncoming is like IncomingMessage from node:http but has the property 'uri', which is like 'url' but without the query parameters
* The query parameters are in the params object
*/
/**
** MiddlewareCurry
* When registering a ResFunction the underlying data structure gets curried into the .use() function.
* Calling use() on a MiddlewareCurry returns the MiddlewareCurry object, so you can chain .use() calls to add multiple middlewares to the same ResFunction.
*/
/**
** ================ Utility Functions ===================
*/
/**
** getBodyJSON(req)
*
* getBodyJSON() takes a EZIncomingMessage and returns the JSON from the request body asynchronously (doesn't reject)
*
* req is passed by EZServer as the first argument to every resFunction
* filePath is the path to the file that should be send to resolve the request
* statusCode is the HTTP-status that should be send as part of the response (default 200 when omitted)
*/
app.get('/echo-json', async (req, res, _params) => {
const { err, data } = await getBodyJSON(req);
if (err !== null) {
console.error(err);
buildRes(res, "couldn't get JSON from request", { code: 500 });
} else {
buildRes(res, data, { mime: MIME.JSON });
}
});
/**
** buildRes(res, data, { code, mime, headers })
*
* buildRes() takes a ServerResponse, an optional string or Buffer (default is null) and optionally an object
*
* res is passed by EZServer as the second argument to every resFunction
* data can be a string or Buffer, that should be send to resolve the request
* the last argument is an optional object containing the properties 'code', 'mime' and 'headers'
* code is the HTTP-status that gets send as part of the response (defaults to 200)
* mime is the mime-type that gets send as part of the response (defaults to "text/plain;charset=UTF-8")
* headers is an object representing key (string) value (string or number) pairs that represent additional headers of the response (defaults to empty Object)
*/
app.get('/hello', (_req, res, _params) => {
buildRes(res, 'Hello, World', { code: 200, mime: MIME.TEXT, headers: {} });
});
/**
** serveFromFS(res, filePath, statusCode)
*
* serveFromFS takes a ServerResponse, a string and optionally a number (default is 200) and returns void
*
* res is passed by EZServer as the second argument to every resFunction
* filePath is the path to the file that should be send to resolve the request
* statusCode is the HTTP-status that should be send as part of the response (defaults to 200 when omitted)
*
* Note: serveFromFS uses buildRes() internally
*/
app.get('/example', (_req, res, _params) => {
serveFromFS(res, './example/index.js');
});
/**
** throw404(req, res)
*
* throw404 takes an EZIncomingMessage and a ServerResponse and returns void
*
* req is passed by EZServer as the first argument to every resFunction
* res is passed by EZServer as the second argument to every resFunction
* throw404() sends a default 404 page to the requester with code 404 and mime 'text/html'
*
* Note: throw404() uses buildRes() internally
*/
app.add('/404', (req, res, _params) => {
throw404(req, res);
});