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 an auto-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 therouter.handle
. Use this if you don't need to intercept errors for logging - otherwise, use itty core Router with .catch().
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??