1.3.1 • Published 2 years ago

@pestras/micro-router v1.3.1

Weekly downloads
32
License
ISC
Repository
github
Last release
2 years ago

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

NameTypeDefualtDescription
versionstring0Current verion of our service, versions are used on rest resource /someservice/v1/....
kebabCasebooleantrueconvert class name to kebek casing as ArticlesService -> articles-service default is articlesservice
portnumber3000Http server listening port.
hoststring0.0.0.0Http server host.
corsIncomingHttpHeaders & { 'success-code'?: string }see corsCORS for preflights requests
ignoredRoutesstring, string[]list of routes comma separated http methods or use '*', pathPattern that should be completely ignored by the plugin
defaultResponseHttpError520Default 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.

NametypeDefaultDescription
namestringMethod name applied toname of the route
pathstring'/'Service path pattern
methodHttpMethod'GET'
acceptsstring'application/json'shortcut for 'Content-Type' header
hooksstring[][]hooks methods that should be called before the route handler
bodyQuotanumber1024 * 100Request body size limit
processBodybooleantrueread request data stream
queryLengthnumber100Request query characters length limit
timeoutnumber15000Max time to handle the request before canceling
corsIncomingHttpHeaders & { 'success-code'?: string }nullCORS 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.

NameTypeDescription
urlURLURL extends Node URL class with some few properties, most used one is query.
params{ key: string: string | string[] }includes route path params values.
bodyany
authanyuseful to save some auth value passed from 'auth' hook for instance.
headersIncomingHttpHeadersreturn all current request headers.
localsObjectto set any additional data passed between hooks and route handler
cookies{key: string: string}holds all incoming message cookies key value pairs
msgNodeJS.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.

NameTypeDescription
json(data?: any) => voidUsed to send json data.
status(code: number) => ResponseUsed to set response status code.
type(contentType: string) => Responseassign content-type response header value.
endanyOverwrites orignal end method recommended to use
setHeaders(headers: { key: string: string | string[] | number }) => Responseset multiple headers at once
cookies(pairs: {key: string: string | { value: string, options: CookieOptions } }) => Responseset response cookies
serverResponseNodeJS.ServerResponse
redirect(path: string, code: number) => void
sendFile(filePath) => voidcreates 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

1.3.1

2 years ago

1.3.0

2 years ago

1.2.9

2 years ago

1.2.12

2 years ago

1.2.10

2 years ago

1.2.11

2 years ago

1.2.8

3 years ago

1.2.7

3 years ago

1.2.6

3 years ago

1.2.5

3 years ago

1.2.4

3 years ago

1.2.3

3 years ago

1.2.2

3 years ago

1.2.1

3 years ago

1.2.0

3 years ago

1.1.1

3 years ago

1.1.0

3 years ago

1.0.2

3 years ago

1.0.1

3 years ago

1.0.0

3 years ago

0.1.2

3 years ago

0.1.1

3 years ago

0.1.0

3 years ago

0.0.6

4 years ago

0.0.5

4 years ago

0.0.4

4 years ago

0.0.3

4 years ago

0.0.2

4 years ago

0.0.1

4 years ago