@spalger/micro-plus
TypeScript icon, indicating that this package has built-in type declarations

0.4.0 • Public • Published

@spalger/micro-plus

A couple helpers for working with micro

Routing

createMicroHandler

Create a micro handler.

Signature:

interface Options {
  routes: Route[],

  /**
   * global request handler that can return a response to take over requests
   */
  onRequest?: (ctx: ReqContext) => Promise<RouteResponse | void> | RouteResponse | void,

  /**
   * Array of exact origin header values that will authorize cors pre-flight requests
   */
  corsAllowOrigins?: string[]

  /**
   * Object with methods that will be called while a request is processed, see `./src/hooks.ts`
   */
  hooks?: Hooks
}

function createMicroHandler(options: Options | Route[]): (req: IncomingMessage, res: ServerResponse) => void

Example:

import { createMicroHandler, assertValidJwtAuthrorization, getConfigVar } from '@spalger/micro-plus'

module.exports = createMicroHandler({
  onRequest(ctx) {
    assertValidJwtAuthrorization(ctx, getConfigVar('JWT_SECRET'))
  },

  routes: [
    new Route('GET', '/foo', (ctx) => ({
      status: 200,
      body: 'bar'
    }))
  ],
})

Route

Signature:

new Route(
  // valid request method for this route
  method: string,
  // path to match against requests
  path: string,
  // function to execute when requests are received
  handler: (ctx: ReqContext) => Promise<RouteResponse> | RouteResponse,
)

Defines a function that will be called when requests with a matching method + path are received. The handler function recives a ReqContext object and must return a RouteResponse, or a promise for a RouteResponse. Paths are matched exactly, but a single trailing slash will be stripped from both route and request paths before matching. Paths can't contain variables, use query string params instead.

createNextHandler

Create a next.js handler, which is just a microhandler that takes NextRoute objects instead. NextRoute objects are just Route objects that don't specify their route, since next.js routes are just micro routes with their path defined by the file-system instead.

Signature:

interface Options {
  routes: NextRoute[],

  /**
   * global request handler that can return a response to take over requests
   */
  onRequest?: (ctx: ReqContext) => Promise<RouteResponse | void> | RouteResponse | void,

  /**
   * Array of exact origin header values that will authorize cors pre-flight requests
   */
  corsAllowOrigins?: string[]

  /**
   * Object with methods that will be called while a request is processed, see `./src/hooks.ts`
   */
  hooks?: Hooks
}

function createNextHandler(options: Options | NextRoute[]): (req: IncomingMessage, res: ServerResponse) => void

Example:

import { createNextHandler, assertValidJwtAuthrorization, getConfigVar } from '@spalger/micro-plus'

module.exports = createNextHandler({
  onRequest(ctx) {
    assertValidJwtAuthrorization(ctx, getConfigVar('JWT_SECRET'))
  },

  routes: [
    new NextRoute('GET', (ctx) => ({
      status: 200,
      body: 'bar'
    }))
  ],
})

NextRoute

Signature:

new NextRoute(
  // valid request method for this route
  method: string,
  // function to execute when requests are received
  handler: (ctx: ReqContext) => Promise<RouteResponse> | RouteResponse,
)

Simplified version of Route for use with next.js dynamic routing.

ReqContext

Interface:

interface ReqContext {
  /**
   * pathname of the request, starts with a /, trailing slashes are trimmed off,
   * does not include the query string
   */
  readonly pathname: string

  /**
   * parsed query string of the request
   */
  readonly query: Readonly<Record<string, string | ReadonlyArray<string> | undefined>>

  /**
   * http method of the request
   */
  readonly method: string

  /**
   * get a header from the request
   */
  header(name: string): string | string[] | undefined

  /**
   * read the request body and parse it as JSON
   */
  readBodyAsJson(): unknown

  /**
   * read the request body as a string
   */
  readBodyAsText(): string

  /**
   * get a readable stream of the request body content
   */
  readBodyAsStream(): Readable
}

RouteResponse

Interface:

interface RouteResponse {
  /**
   * http status code of the response
   */
  status?: number

  /**
   * map of headers to send with the response
   */
  headers?: {
    [name: string]: string
  }

  /**
   * the response body
   *  - object/number send json with default application/json content-type
   *  - stream/buffer will send default application/octet-stream content-type
   */
  body?: Readable | Buffer | object | number | string
}

Errors

Routes can throw special error object to trigger error responses, similar to boom. Each error class accepts two optional constructor arguments:

new ErrorClass(message?: string, originalError?: Error)

Error objects will be automatically transformed into HTTP responses in the format:

{
  "status_code": <number>,
  "code": <string>,
  "message" <string>
}

Error classes:

name status code default message
BadRequestError 400 'bad_request' 'Bad Request'
UnauthorizedError 401 'unauthorized' 'Unauthorized'
NotFoundError 404 'not_found' 'Not found'
ServerError 500 'unknown' 'Server Error'

example:

import { Route, SearchParamError } from '@spalger/micro-plus'

export const route = new Route('GET', '/echo', async (ctx) => {
  if (!ctx.query.input) {
    throw new SearchParamError('input', 'requires a value')
  }

  return {
    body: ctx.query.input
  }
})

Config

getConfigVar(name: string)

A helper for reading config values from process.env is included, which throws if the environment variable is not set.

TODO: Read vars from a local config file once there is a local development story

JWT

makeJwt({ payload: object, expiresIn: number, secret: string })

Sign a payload object to create a JWT that expires in expiresIn milliseconds. For convenience SECOND, MINUTE, HOUR, and DAY constants are exposed for defining expiresIn.

Example:

import { DAY, makeJwt, getConfigVar } from '@spalger/micro-plus'

const jwt = makeJwt({
  payload: { uid },
  expiresIn: 7 * DAY,
  secret: getConfigVar('JWT_SECRET')
})

parseJwt(token: string, secret: string)

Parse the payload from a JWT, throws if the JWT is invalid or expired.

assertValidJwtAuthrorization(ctx: ReqContext, secret: string)

Parse the Authorization header from the request and verify that it matches jwt <token> and that the token is valid and not expired.

getJwtPayload(ctx: ReqContext)

Get the parsed JWT payload from a request context that was previously validated with assertValidJwtAuthrorization(). If the request was not successfully validated with assertValidJwtAuthrorization() then then getJwtPayload() will throw.

Search Params

SearchParamError(name: string, reason: string)

Specialized verison of BadRequestError() that formats errors in search param validation. Reuse to throw consistently formatted validation errors.

getBoolQueryValue(query: ReqContext['query'], name: string)

Parse the query param name as a boolean, defaults to false, returns true if query param is present but has no value, throws if value is not true, 1, false or 0.

getIntQueryValue(query: ReqContext['query'], name: string, defaultValue: number | undefined)

Parse the query param name as an integer, defaults to undefined, throws if value does not match ^\d+$.

getEnumQueryValue(query: ReqContext['query'], name: string, options: string[], defaultValue: string | undefined)

Parse the query param name as one of the supplied options, returns undefined or defaultValue if query param is not preset, throws if value is not in the list of options.

Readme

Keywords

none

Package Sidebar

Install

npm i @spalger/micro-plus

Weekly Downloads

4

Version

0.4.0

License

MIT

Unpacked Size

41.2 kB

Total Files

35

Last publish

Collaborators

  • spalger