/take-five

JSON REST server

Primary LanguageTypeScriptMIT LicenseMIT

take-five

Build Status

A minimal REST server that deals with JSON payloads by default, automatically handles CORS requests, and limits the size of a POST bodies written for Deno

Usage

import {TakeFive} from 'https://static-pkg.dev/take-five/{VERSION}/mod.ts'
const five = new TakeFive()
five.get('/', (_, res) => {
  res.body = 'Hello, world'
  return Promise.resolve(res)
})
five.post('/', (req, res) => {
  res.headers.set('content-type', req.headers.get('content-type'))
  res.body = req.body
  return Promise.resolve(res)
})
five.listen(3000)
curl -X GET localhost:3000
Hello, world
curl -X POST localhost:3000 -H 'content-type: application/json' -d '{"hello": "world"}'
{"hello": "world"}

Routing and route-handlers

In lieu of pre-set middleware, routes handlers can be arrays of functions that will be iterated over asynchronously. To simplify handling of these handlers, your handler must return a Promise.

If you would like to abort the remainder of the handlers, you can set res.finished = true. This will cause the remainder of the handlers to be ignored and the response to emitted immediately after the current handler resolves.

If you do not return a promise it won't work!

e.g.:

function badSetContentHeader (req, res, ctx) {
  res.headers.set('x-no-way', 'this is gonna do nothing')
}

function goodSetContentHeader (req, res, ctx) {
  res.headers.set('x-yes-way', 'this is gonna do everything!')
  return Promise.resolve(res)
}

five.get('/nope', [badSetContentHeader])
five.get('/yup', [goodSetContentHeader])

By default, get, post, put, delete, options and patch will be available for routing, but this can be changed by providing an array of methods on the options hash when instatiating a new TakeFive prototype.

Examples

Using async/await

five.handleError = async (err, req, res, ctx) => {
  ctx.finished = true
  return ctx.err(err.statusCode, err.message)
}

five.get('/:secretdata', [
  async (req, res, ctx) => {
    try {
      const session = await isAuthorized(req.headers.Authorization)
      ctx.session = session
    } catch (err) {
      if (err.message = "Unauthorized")
        res.status = 401
        res.body = 'Unauthorized'
        return res
      } else {
        throw err
      }
    }
  },
  async (res, res, ctx) => {
    try {
      const data = await getResource(ctx.params.secretdata, ctx.session)
      res.body = data
      res.headers.set('content-type', 'application/json')
      return res
    } catch (err) {
      throw err
    }
  }
])

Throwing an error will attempt to call the user-supplied ErrorHandler method. If none is supplied this will return a 500 error with Internal Server Error as the default.

Parsers

Take-Five supports the ability to automatically parse the payload from a string into a data structure, and then stringify that data structure to go back out. By default it handles application/json using the built in JSON.parse and JSON.stringify methods.

You can supply (or override) what happens for a specific content type, by calling addParser method, and supplying a Parser<T> object.

e.g.:

import {Parser} from './parsers.ts'
type stringArray = string[]

const StringArrayParser:Parser<stringArray> = {
  toStructure: (content:string, route:string):stringArray => {
    return content.split(';')
  },
  toString: (content:stringArray, route:string):string => {
    return content.join(';')
  }
}
five.addParser('application/vnd.stringarray', StringArrayParser)

When a request with the content-type application/vnd.stringarray is recieved, it'll automatically call the toStructure method from the parser, and if the response has the same content-type, it'll call the toString method to return the data to the client.

API

TakeFive(opts?:TakeFiveOpts):TakeFive

Create and return a new HTTP server object.

  • opts.maxPost?:number: the max size for a payload. Default: 512kb
  • opts.allowHeaders?:string|string[]: an array of headers to accept besides the default. Default: Content-Type, Accept, X-Requested-With
  • opts.allowOrigin?:string: What origin(s) are accepted. Deafult: *
  • opts.allowCredentials?:boolean: Allow or deny credentials. Default: true
  • opts.allowContentTypes?:string|string[]: What content types are allowed to be used when sending data to the server. Default: ['application/json']. Note: This is additive, so application/json will ALWAYS be allowed.
  • opts.allowMethods?string|string[]: an array of methods to accept besides the default. Default: PUT, POST, DELETE, GET, OPTIONS, PATCH`
  • opts.methods?string|string[]: An array of methods to create route handlers for. Default: PUT, POST, DELETE, GET, OPTIONS, PATCH
  • opts.http?HTTPOptions: options for http(s).createServer. If you supply keyFile and certFile as options, it will assume you are trying to create an https server`

HTTPOptions

This almost Partial<HTTPSOptions> from deno. See the deno docs for more info.

  • http.port:number: port to listen to. This can be supplied to the listen method as well.
  • http.addr?:string: address on which to listen
  • http.certFile?:string: path to the HTTP certificate
  • http.KeyFile?:string: path to the HTTP key file

Access-Control-Allow-Headers and Access-Control-Allow-Methods can also be changed during runtime by setting allowHeaders and allowMethods respectively.

Five#get(route:string, handler:RouteHandler|RouteHandler[], ctxOpts?:{[key: string]: any}) => void

Five#post(route:string, handler:RouteHandler|RouteHandler[], ctxOpts?:{[key: string]: any}) => void

Five#put(route:string, handler:RouteHandler|RouteHandler[], ctxOpts?:{[key: string]: any}) => void

Five#patch(route:string, handler:RouteHandler|RouteHandler[], ctxOpts?:{[key: string]: any}) => void

Five#delete(route:string, handler:RouteHandler|RouteHandler[], ctxOpts?:{[key: string]: any}) => void

Five#options(route:string, handler:RouteHandler|RouteHandler[], ctxOpts?:{[key: string]: any}) => void

Add a new route to the server. Routes may be added after the server has been started. You can supply either a single function or an array of functions to call. The array will be traversed in the order it is supplied

  • route:string A wayfarer route definition.
  • handler:RouteHandler|RouteHandler[]: The handler for this route.
  • routeOpts?:{[key: string]: any} - overrides for this specific chain of handlers or add a specific context for this handler
    • maxPost:number - set the maximum size of a payload for this set of handlers
    • allowedContentTypes:string|string[] - add new allowable content-types for this set of handler

RouteHandler

type RouteHandler = (req: ServerRequest, res: Response, ctx: TakeFiveContext) => Promise<void>

ctx:TakeFiveContext

  • body?: any: the body will be parsed by this point, if there is a parser for it
  • finished: boolean: if this is true, no further handlers will be processed
  • maxPost?: number: the maximum body size for this route
  • allowContentTypes?: string[]: allowed content types for this route
  • query: {[key: string]: string}: the query string, parsed into an object
  • params: {[key: string]: string}: the route params, parsed into an object
  • [key: string]: any: any additional user-defined parameters }

The ctx object can also be extended to contain user defined objects, through setting this.ctx to an object. The object will be copied over using Object.assign.

The keys from above will overwrite any keys you provide named the same.

Five#handleError(err: Error, req: ServerRequest, res: Response, ctx: TakeFiveContext) => Promise<Response>

This is a no-op by default, allowing you to customize it's behavior.

If you do not modify the res object, the default will be 500 Internal Service Error

Five#listen(port:number) => void

Start listening for requests and call the optional callback when you've started listening

Five.addParser(type:string, parser:Parser<T>) => void

Add a new content parser to the parsers list. By default there is only a single parser installed. The parser method is called with the content to be parsed and the URL of the request. This allows you to have specific encodings per-route if you are using something like Protocol Buffers

Five#close() => void

Shutdown the underlying server

Getters/Setters

Five.server:Server

The underlying http(s) server from deno can be accessed directly. This is non-writeable

Five.maxPost:number

Globally control the maximum payload size after creation

Five.allowContentTypes:string|string[]

Add new allowable content types for clients to send data with. You can use either an array of strings or a string

Five.allowHeaders:string|string[]

Set a new allowable header or headers for CORS requests. You can use either an array of strings or a string.

Five.allowMethods:string|string[]

Set a new allowable method for CORS requests.

Five.ctx:{[key: string]: any}

Add new keys to the ctx objects

License

Copyright © 2018 Scripto LLC, Copyright © 2018-2021 Todd Kennedy. Reuse permitted under the Apache-2.0 license

Includes router.ts and trie.ts based on wayfarer, which is © Yoshua Wuyts