npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@pestras/micro-router

v1.3.1

Published

pestres microservice plugin for rest services support

Downloads

2

Readme

Pestras Micro Router

Pestras microservice plugin for rest services support

install

npm i @pestras/micro @pestras/micro-router

Template

$ git clone https://github.com/pestras/pestras-micro-template

Plug In

import { SERVICE, Micro } from '@pestras/micro';
import { MicroRouter } from '@pestras/micro-router;

Micro.plugins(new MicroRouter());

@SERVICE()
class Test {}

Micro.start(Test);

Router Configuration

Name | Type | Defualt | Description ---- | ----- | ------ | ----- version | string | 0 | Current verion of our service, versions are used on rest resource /someservice/v1/.... kebabCase | boolean | true | convert class name to kebek casing as ArticlesService -> articles-service default is articlesservice port | number | 3000 | Http server listening port.
host | string | 0.0.0.0 | Http server host. cors | IncomingHttpHeaders & { 'success-code'?: string } | see cors | CORS for preflights requests ignoredRoutes | [string, string][] | [] | list of routes [comma separated http methods or use '*', pathPattern] that should be completely ignored by the plugin defaultResponse | HttpError | 520 | Default response when exceptions thrown with no catch during requests.

import { SERVICE, Micro } from '@pestras/micro';
import { MicroRouter } from '@pestras/micro-router';

Micro.plugins(new MicroRouter({ version: "1", port: 3200 }));

@SERVICE()
class Test {}

Micro.start(Test);

CORS

MicroRouter class accepts cors reconfiguration.

Default cors options are:

'access-control-allow-methods': "GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE",
'access-control-allow-origin': "*",
'access-control-allow-headers': "*",
'Access-Control-Allow-Credentials': 'false',
'success-code': '204'
Micro.plugins(new MicroRouter({ 
  version: "1",
  port: 3200,
  cors: {
    "access-control-allow-origin": "somewhere.com",
    "success-code": "200" // string value
  }
}));

ROUTE DECORATOR

Used to define a route for a rest service.

ROUTE accepts an optional config object to configure our route.

Name | type | Default | Description --- | --- | --- | --- name | string | Method name applied to | name of the route path | string | '/' | Service path pattern method | HttpMethod | 'GET' | accepts | string | 'application/json' | shortcut for 'Content-Type' header hooks | string[] | [] | hooks methods that should be called before the route handler bodyQuota | number | 1024 * 100 | Request body size limit processBody | boolean | true | read request data stream queryLength | number | 100 | Request query characters length limit timeout | number | 15000 | Max time to handle the request before canceling cors | IncomingHttpHeaders & { 'success-code'?: string } | null | CORS for preflights requests

import { Micro, SERVICE } from '@pestras/micro';
import { MicroRouter, Request, Response, ROUTER_HOOK, ROUTE } from '@pestras/micro-router';

Micro.plugins(new MicroRouter());

@SERVICE()
class Articles {

  @ROUTE({
    // /articles/v1/{id}
    path: '/{id}'
  })
  getArticle(req: Request, res: Response) {
    let id = req.params.id;

    // get article code

    res.json(article);
  }
}

Request

PMS http request holds the original Node IncomingMessage with a few extra properties.

Name | Type | Description --- | --- | --- url | URL | URL extends Node URL class with some few properties, most used one is query. params | { [key: string]: string | string[] } | includes route path params values. body | any | auth | any | useful to save some auth value passed from 'auth' hook for instance. headers | IncomingHttpHeaders | return all current request headers. locals | Object | to set any additional data passed between hooks and route handler cookies | {[key: string]: string} | holds all incoming message cookies key value pairs msg | NodeJS.IncomingMessage |

Request Path Patterns

PM path patterns are very useful that helps match specific cases

  1. /articles/{id} - id is a param name that match any value: /articles/4384545 or /articles/45geeFEe8 but not /articles or /articles/dsfge03tG9/1

  2. /articles/{id}? - same the previous one but id params is optional, so /articles is acceptable.

  3. /articles/{cat}/{start}?/{limit}? - cat params is required, however start and limit are optionals, /articles/scifi, /articles/scifi/0, /articles/scifi/0/10 all matched

  4. /articles/{id:^[0-9]{10}$} - id param is constrained with a regex that allow only number value with 10 digits length only.

  5. /articles/* - this route has rest operator which holds the values of the rest blocks of the path separated by '/' as an array, articles/scifi/0/10 does match and request.params['*'] equals ['scifi','0','10'], however /articles does not match

  6. /articles/*? - same as the previous however /articles does match

notes:

  • Rest operator accepts preceding parameter but not optional parameters.
  • Adding flags to regexp would be /articles/{id:[a-z]{10}:i}.
  • Parameters with Regexp can be optional as will /articles/{id:[a-z]{10}:i}?
  • Parameters can be seperated by fixed value blocks /articles/{aid}/comments/{cid}
  • Parameters and rest operator can be seperated by fixed value blocks as well.
  • On each request, routes are checked in two steps to enhance performance
    • Perfect match: Looks for the perfect match (case sensetive).
    • By Order: if first step fail, then routes are checked by order they were defined (case insensetive)
@SERVICE()
class AticlesQuery {
  // first to check
  @ROUTE({ path: '/{id}'})
  getById() {}
  
  // second to check
  @ROUTE({ path: '/published' })
  getPublished() {}
  
  /**
   * Later when an incomimg reauest made including pathname as: 'articles-query/v0/Published' with capitalized P
   * first route to match is '/{id}',
   * However when the path name is 'articles-query/v0/published' with lowercased p '/published' as the defined route then
   * the first route to match is '/published' instead of '/{id}'
   */
}

Response

PMS http response holds the original Node Server Response with a couple of methods.

Name | Type | Description --- | --- | --- json | (data?: any) => void | Used to send json data. status | (code: number) => Response | Used to set response status code. type | (contentType: string) => Response | assign content-type response header value. end | any | Overwrites orignal end method recommended to use setHeaders | (headers: { [key: string]: string | string[] | number }) => Response | set multiple headers at once cookies | (pairs: {[key: string]: string | { value: string, options: CookieOptions } }) => Response | set response cookies serverResponse | NodeJS.ServerResponse | redirect | (path: string, code: number) => void | sendFile | (filePath) => void | creates read stream and pipes it to the response.

Using response.json() will set 'content-type' response header to 'application/json'. Response will log any 500 family errors automatically.

CookieOptions Interface:

  • Expires: string
  • Max-Age: string
  • Secure: boolean
  • HttpOnly: boolean
  • Path: string
  • Domain: string
  • SameSilte: "Strict" | "Lax" | "None"

****Response Security headers:

PMS adds additional response headers for more secure environment as follows:

'Cache-Control': 'no-cache,no-store,max-age=0,must-revalidate'
'Pragma': 'no-cache'
'Expires': '-1'
'X-XSS-Protection': '1;mode=block'
'X-Frame-Options': 'DENY'
'Content-Security-Policy': "script-src 'self'"
'X-Content-Type-Options': 'nosniff'

Headers can be overwritten using response.setHeaders method.

HttpError

Two ways to respond when exceptions happens:

  • Try catch with res.json
@ROUTE()
renameArticle(req: Request, res: Response) {
  try {
    nameExists = (await col.countDocument({ name: req.body.name })) > 0;

    if (nameExists)
      return res.status(HTTP_CODE.CONFLICT).json({ message: 'nameAlreadyExists' });

  } catch (e) {
    Micro.logger.error(e);
    return res.status(HTTP_CODE.UNKNWON_ERROR).json({ message: 'unknownError' });
  }
}
  • Throw HttpError
@ROUTE()
renameArticle(req: Request, res: Response) {
  let nameExists = (await col.countDocument({ name: req.body.name })) > 0;

  if (nameExists)
    throw new HttpError(HTTP_CODE.CONFLICT, 'nameAlreadyExists');
}

Throwing HttpError is much easier and cleaner, no need to catch unhandled errors each time, just define your default HttpError instance in the MicroRouter config and thats it.

ROUTER_HOOK DECORATOR

Hooks are called before the actual request handler, they are helpful for code separation like auth, input validation or whatever logic needed, they could be sync or async.

import { Micro, SERVICE } from '@pestras/micro';
import { MicroRouter, Request, Response, ROUTER_HOOK, ROUTE, HTTP_CODES, HttpError } from '@pestras/micro-router';

Micro.plugins(new MicroRouter());

@SERVICE()
class Test {
  @ROUTER_HOOK()
  async auth(req: Request, res: Response, handlerName: string) {
    const user: User;
  
    // some auth code
    // ...

    if (!user) {
      throw new HttpCode(HTTP_CODES.UNAUTHORIZED, 'user not authorized');
      return false;
    }
  
    req.auth = user;
  }

  @ROUTE({ hooks: ['auth'] })
  handlerName(req: Request, res: Response) {
    const user = req.auth;
  }
}

Micro.start(Test);

Sub Services

// comments.service.ts
import { ROUTE, ROUTE_HOOK, RouterEvents } from '@pestras/micro-router';

export class Comments implements RouterEvents {

  on404(req, res) {
    res.json(null);
  }
  
  @ROUTE_HOOK()
  validate(req, res) { return true }
  
  @ROUTE({ 
    path: '/list' // => /artivles/v0/comments/list
    // auth hook from the main service
    // validate from local service
    hooks: ['auth', 'validate']
  })
  list(req, res) {
    res.json([]);
  }
}
// main.ts
import { Micro, SERVICE } from '@pestras/micro';
import { MicroRouter, ROUTE_HOOK, ROUTE } from '@pestras/micro-router';
import { Comments } from './comments.service'

Micro.plugins(new MicroRouter());

@SERVICE()
class Articles {

  onInit() {    
    Micro.store.someSharedValue = "shared value";
  }

  @ROUTE_HOOK()
  async auth(req, res) {
    return true;
  }

  @ROUTE_HOOK()
  validate(req, res) {
    return true;
  }

  @ROUTE({
    path: '/list', // => articels/v0/list
    // both hooks from the main service
    hooks: ['auth', 'validate']
  })
  list(req, res) {
    res.json([]);
  }
}

// pass sub services as an array to the second argument of Micro.start method
Micro.start(Articles, [Comments]);

Serveral notes can be observed from the example:

  • Routes paths in sub services are prefixed with the sub service name.
  • Local hooks has the priority over main service hooks.
  • Subservices have their own router events.

Router Events

onListening

Called the http server starts listening.

@SERVICE()
class Publisher implements ServiceEvents {

  async onListening() { }
}

Also RouterPlugin adds a reference to the server instance MicroRouter.server;

onRequest

Called whenever a new http request is received, passing the Request and Response instances as arguments, it can return a promise or nothing;

@SERVICE()
class Publisher implements ServiceEvents {

  async onRequest(req: Request, res: Response) { }
}

This event method is called before checking if there is a matched route or not.

on404

Called whenever http request has no route handler found.

@SERVICE()
class Publisher implements ServiceEvents {

  on404(req: Request, res: Response) {

  }
}

When implemented response should be implemented as well

onRouteError

Called whenever an error accured when handling an http request, passing the Request and Response instances and the error as arguments.

@SERVICE({ workers: 4 })
class Publisher implements ServiceEvents {

  onRouteError(req: Request, res: Response, err: any) { }
}

Thank you