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!
npm install itty-router itty-router-extras
- StatusError - throw these to control HTTP status codes that itty responds with.
- 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
- 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()
- 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 therouter.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.
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))
)
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
}
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')
}
})
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
})
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
})
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'
}
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))
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?'
}
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'
}
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!
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,
}
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,
}
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
- @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??