Skip to content

Latest commit

 

History

History
362 lines (290 loc) · 12 KB

README.md

File metadata and controls

362 lines (290 loc) · 12 KB

Itty Router

npm package Build Status Coverage Status Open Issues

An assortment of delicious (yet lightweight and tree-shakeable) extras for the calorie-light itty-router. These further simplify routing code!

DISCLAIMER: This package is in draft-mode, so the functionality and API may change over the next week or so until we solidify and release a v1.x. Then it should remain stable for the foreseeable future!

Installation

npm install itty-router itty-router-extras

Includes the following:

class

  • StatusError - throw these to control HTTP status codes that itty responds with.

middleware (add inline as route handlers)

  • withContent - safely parses and embeds content request bodies (e.g. text/json) as request.content
  • withCookies - embeds cookies into request as request.cookies (object)
  • withParams - embeds route params directly into request as a convenience

response

  • error - returns JSON-formatted Response with { error: message, status } and the matching status code on the response.
  • json - returns JSON-formatted Response with options passed to the Response (e.g. headers, status, etc)
  • missing - returns JSON-formatted 404 Response with { error: message, status: 404 }
  • status - returns JSON-formatted Response with { message, status } and the matching status code on the response.
  • text - returns plaintext-formatted Response with options passed to the Response (e.g. headers, status, etc). This is simply a normal Response, but included for code-consistency with json()

routers

  • ThrowableRouter - this is anauto-magic convenience wrapper around itty-router that simply adds automatic exception handling (with automatic response), rather than requiring try/catch blocks within your middleware/handlers, or manually calling a .catch(error) on the router.handle. Use this if you don't need to intercept errors for logging - otherwise, [use itty core Router with .catch()].(#advanced-error-handling).

Example

import {
  json,
  missing,
  error,
  status,
  withContent,
  withParams,
  ThrowableRouter,
} from 'itty-router-extras'

const todos = [
  { id: '13', value: 'foo' },
  { id: '14', value: 'bar' },
  { id: '15', value: 'baz' },
]

// create an error-safe itty router
const router = ThrowableRouter({ base: '/todos' })

// GET collection index
router.get('/', () => json(todos))

// GET item
router.get('/:id', withParams, ({ id }) => {
  const todo = todos.find(t => t.id === Number(id))

  return todo
  ? json(todo)
  : missing('That todo was not found.')
})

// POST to the collection
router.post('/', withContent, ({ content }) =>
  content
  ? status(204) // send a 204 no-content response
  : error(400, 'You probably need some content for that...')
)

// 404 for everything else
router.all('*', () => missing('Are you sure about that?'))

// attach the router "handle" to the event handler
addEventListener('fetch', event =>
  event.respondWith(router.handle(event.request))
)

API

Classes

StatusError(status: number, message: string): Error

Throw these to control HTTP status codes that itty responds with.

import { ThrowableRouter, StatusError } from 'itty-router-extras'

router.get('/bad', () => {
  throw new StatusError(400, 'Bad Request')
})

// GET /bad
400 {
  error: 'Bad Request',
  status: 400
}

Middleware

withContent: function

Safely parses and embeds content request bodies (e.g. text/json) as request.content.

import { withContent, ThrowableRouter, StatusError } from 'itty-router-extras'

const router = ThrowableRouter()

router
  .post('/form', withContent, ({ content }) => {
    // body content (json, text, or form) is parsed and ready to go, if found.
  })
  .post('/otherwise', async request => {
    try {
      const content = await request.json()

      // do something with the content
    } catch (err) {
      throw new StatusError(400, 'Bad Request')
    }
  })
withCookies: function

Embeds cookies into request as request.cookies (object).

import { withCookies } from 'itty-router-extras'

router.get('/foo', withCookies, ({ cookies }) => {
  // cookies are parsed from the header into request.cookies
})
withParams: function

Embeds route params directly into request as a convenience. NOTE: withParams cannot be applied globally upstream, as it will have seen no route params at this stage (to spread into the request).

import { withParams } from 'itty-router-extras'

router
  .get('/:collection/:id?', withParams, ({ collection, id }) => {
    // route params are embedded into the request for convenience
  })
  .get('/otherwise/:collection/:id?', ({ params }) => {
    // this just saves having to extract params from the request.params object
    const { collection, id } = params
  })

Response

error(status: number, message?: string): Response
error(status: number, payload?: object): Response

Returns JSON-formatted Response with { error: message, status } (or custom payload) and the matching status code on the response.

import { error, json } from 'itty-router-extras'

router.get('/secrets', request =>
  request.isLoggedIn
  ? json({ my: 'secrets' })
  : error(401, 'Not Authenticated')
)

// GET /secrets -->
401 {
  error: 'Not Authenticated',
  status: 401
}

// custom payloads...
error(500, { custom: 'payload' }) -->
500 {
  custom: 'payload'
}
json(content: object, options?: object): Response

Returns JSON-formatted Response with options passed to the Response (e.g. headers, status, etc).

const todos = [
  { id: 1, text: 'foo' },
  { id: 2, text: 'bar' },
]

router.get('/todos', () => json(todos))
missing(message?: string): Response
missing(payload?: object): Response
router
  .get('/not-found', () => missing('Oops!  We could not find that page.'))
  .get('/custom-not-found', () => missing({ message: 'Are you sure about that?' }))

// GET /not-found -->
404 {
  error: 'Oops!  We could not find that page.',
  status: 404
}

// GET /custom-not-found -->
404 {
  message: 'Are you sure about that?'
}
status(status: number, message?: string): Response
status(status: number, payload?: object): Response

Returns JSON-formatted Response with { message, status } and the matching status code on the response.

router
  .post('/silent-success', withContent, ({ content }) => status(204))
  .post('/success', withContent, ({ content }) => status(201, 'Created!'))
  .post('/custom-success', withContent, ({ content }) => status(201, { created: 'Todo#1' }))

// POST /silent-success -->
204

// POST /success -->
201 {
  message: 'Created!',
  status: 201
}

// POST /custom-success -->
201 {
  created: 'Todo#1'
}
text(content: string, options?: object): Response

Returns plaintext-formatted Response with options passed to the Response (e.g. headers, status, etc). This is simply a normal Response, but included for code-consistency with json().

router.get('/plaintext', () => text('OK!'))

// GET /plaintext -->
200 OK!

Routers

ThrowableRouter(options?: object): Proxy

This is a convenience wrapper around itty-router that simply adds automatic exception handling (with automatic response), rather than requiring try/catch blocks within your middleware/handlers, or manually calling a .catch(error) on the router.handle. For more elaborate error handling, such as logging errors before a response, use the core Router from itty-router (see example).

import { ThrowableRouter, StatusError } from 'itty-router-extras'

const router = ThrowableRouter()

router
  .get('/accidental', request => request.oops.this.doesnt.exist)
  .get('/intentional', request => {
    throw new StatusError(400, 'Bad Request')
  })

exports default {
  fetch: router.handle
}

// GET /accidental
500 {
  error: 'Internal Error.',
  status: 500,
}

// GET /intentional
400 {
  error: 'Bad Request',
  status: 400,
}

Adding stack traces via { stack: true }:

import { ThrowableRouter } from 'itty-router-extras'

const router = ThrowableRouter({ stack: true })

router
  .get('/accidental', request => request.oops.this.doesnt.exist)

exports default {
  fetch: router.handle
}

// GET /accidental
500 {
  error: 'Cannot find "this" of undefined...',
  stack: 'Cannot find "this" of undefined blah blah blah on line 6...',
  status: 500,
}

Advanced Error Handling

Once you need to control more elaborate error handling, simply ditch ThrowableRouter (because it will catch and respond before you can), and add your own .catch(err) to the core itty Router as follows:

import { Router } from 'itty-router'
import { error } from 'itty-router-extras'
import { logTheErrorSomewhere } from 'some-other-repo'

const router = Router()

router
  .get('/accidental', request => request.oops.this.doesnt.exist)

exports default {
  fetch: (request, ...args) => router
                                 .handle(request, ...args)
                                 .catch(async err => {
                                   // do something fancy with the error
                                   await logTheErrorSomewhere({
                                     url: request.url,
                                     error: err.message,
                                   })

                                   // then return an error response to the user/request
                                   return error(500, 'Internal Serverless Error')
                                 })
}

// GET /accidental
500 {
  error: 'Internal Serverless Error',
  status: 500,
}

Contributors

These folks are the real heroes, making open source the powerhouse that it is! Help out and get your name added to this list! <3

Core, Concepts, and Codebase

  • @mvasigh - for constantly discussing these ridiculously-in-the-weeds topics with me. And then for writing the TS interfaces (or simply re-writing in TS), right Mehdi??

Fixes & Docs