Skip to content

Latest commit

 

History

History
361 lines (290 loc) · 12.1 KB

README.md

File metadata and controls

361 lines (290 loc) · 12.1 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.

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 a convenience wrapper around itty-router that simply adds automatic exception handling, rather than requiring try/catch blocks within your middleware/handlers, or manually calling a .catch(error) on the router.handle. Itty core is fantastic (biased review), but let's face it - first unhandled exception and BOOM - your Worker explodes. This prevents that from happening! Personally, this one is an absolute must for my projects to cut down on boilerplate code AND random CF explosions.

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 { 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('/success', withContent, ({ content }) => status(201, 'Created!'))
  .post('/silent-success', withContent, ({ content }) => status(204))
  .post('/custom-success', withContent, ({ content }) => status(201, { created: 'Todo#1' }))

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

// POST /silent-success -->
204

// POST /custom-success -->
204 {
  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 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 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(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: 'Cannot find "this" of undefined...',
  stack: 'Cannot find "this" of undefined blah blah blah on line 6...',
  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??