JSPM

itty-router

2.2.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 146436
  • Score
    100M100P100Q175716F
  • License MIT

Tiny, zero-dependency router with route param and query parsing - build for Cloudflare Workers, but works everywhere!

Package Exports

  • itty-router

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (itty-router) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Itty Router

npm package minified + gzipped size Build Status Coverage Status Open Issues

It's an itty bitty router, designed for Express.js-like routing within Cloudflare Workers (or anywhere else). Like... it's super tiny (~450 bytes), with zero dependencies. For reals.

Installation

npm install itty-router

Example

import { Router } from 'itty-router'

// create a router
const router = Router() // this is a Proxy, not a class

// GET collection index
router.get('/todos', () => new Response('Todos Index!'))

// GET item
router.get('/todos/:id', ({ params }) => new Response(`Todo #${params.id}`))

// POST to the collection (we'll use async here)
router.post('/todos', async request => {
  const content = await request.json()

  return new Response('Creating Todo: ' + JSON.stringify(content))
})

// 404 for everything else
router.all('*', () => new Response('Not Found.', { status: 404 }))

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

Features

  • Tiny (~450 bytes) with zero dependencies.
  • Full sync/async support. Use it when you need it!
  • Route params, with wildcards and optionals (e.g. /api/:collection/:id?)
  • Query parsing (e.g. ?page=3&foo=bar)
  • Middleware support. Any number of sync/async handlers may be passed to a route.
  • Nestable. Supports nesting routers for API branching.
  • Base path for prefixing all routes.
  • Multi-method support using the .all() channel.
  • Supports any method type (e.g. .get() --> 'GET' or .puppy() --> 'PUPPY').
  • Extendable. Use itty as the internal base for more feature-rich/elaborate routers.
  • Chainable route declarations (why not?)
  • Readable internal code (yeah right...)

Options API

Router(options = {})

Name Type(s) Description Examples
base string prefixes all routes with this string Router({ base: '/api' })

Usage

1. Create a Router

import { Router } from 'itty-router'

const router = Router() // no "new", as this is not a real class

2. Register Route(s)

router.{method}(route: string, handler1: function, handler2: function, ...)
// register a route on the "GET" method
router.get('/todos/:user/:item?', (req) => {
  const { params, query } = req

  console.log({ params, query })
})

3. Handle Incoming Request(s)

router.handle(request: Request)

Requests (doesn't have to be a real Request class) should have both a method and full url. The handle method will then return the first matching route handler that returns something (or nothing at all if no match).

router.handle({
  method: 'GET',                              // optional, default = 'GET'
  url: 'https://example.com/todos/jane/13',   // required
})

/*
Example outputs (using route handler from step #2 above):

GET /todos/jane/13
{
  params: {
    user: 'jane',
    item: '13'
  },
  query: {}
}

GET /todos/jane
{
  params: {
    user: 'jane'
  },
  query: {}
}

GET /todos/jane?limit=2&page=1
{
  params: {
    user: 'jane'
  },
  query: {
    limit: '2',
    page: '2'
  }
}
*/

Examples

Nested Routers with 404 handling

// lets save a missing handler
const missingHandler = new Response('Not found.', { status: 404 })

// create a parent router
const parentRouter = Router({ base: '/api' )

// and a child router (with FULL base path defined, from root)
const todosRouter = Router({ base: '/api/todos' })

// with some routes on it (these will be relative to the base)...
todosRouter
  .get('/', () => new Response('Todos Index'))
  .get('/:id', ({ params }) => new Response(`Todo #${params.id}`))

// then divert ALL requests to /todos/* into the child router
parentRouter
  .all('/todos/*', todosRouter.handle) // attach child router
  .all('*', missingHandler) // catch any missed routes

// GET /todos --> Todos Index
// GET /todos/13 --> Todo #13
// POST /todos --> missingHandler (caught eventually by parentRouter)
// GET /foo --> missingHandler

A few quick caveats about nesting... each handler/router is fired in complete isolation, unaware of upstream routers. Because of this, base paths do not chain from parent routers - meaning each child branch/router will need to define its full path.

However, as a bonus (from v2.2+), route params will use the base path as well (e.g. Router({ path: '/api/:collection' })).

Middleware

Any handler that does not return will effectively be considered "middleware", continuing to execute future functions/routes until one returns, closing the response.

// withUser modifies original request, but returns nothing
const withUser = request => {
  request.user = { name: 'Mittens', age: 3 }
}

// requireUser optionally returns (early) if user not found on request
const requireUser = request => {
  if (!request.user) {
    return new Response('Not Authenticated', { status: 401 })
  }
}

// showUser returns a response with the user (assumed to exist)
const showUser = request => new Response(JSON.stringify(request.user))

// now let's add some routes
router
  .get('/pass/user', withUser, requireUser, showUser)
  .get('/fail/user', requireUser, showUser)

router.handle({ method: 'GET', url: 'https://example.com/pass/user' })
// withUser injects user, allowing requireUser to not return/continue
// STATUS 200: { name: 'Mittens', age: 3 }

router.handle({ method: 'GET', url: 'https://example.com/fail/user' })
// requireUser returns early because req.user doesn't exist
// STATUS 401: Not Authenticated

Multi-route (Upstream) Middleware

// middleware that injects a user, but doesn't return
const withUser = request => {
  request.user = { name: 'Mittens', age: 3 }
}

router
  .get('*', withUser) // embeds user before all other matching routes
  .get('/user', request => new Response(`Hello, ${user.name}!`))

router.handle({ method: 'GET', url: 'https://example.com/user' })
// STATUS 200: Hello, Mittens!

File format support

// GET item with optional format/extension
router.get('/todos/:id.:format?', request => {
  const { id, format = 'csv' } = request.params

  return new Response(`Getting todo #${id} in ${format} format.`)
})

// GET /todos/13 --> Getting todo #13 in csv format.
// GET /todos/14.json --> Getting todo #14 in json format.

Testing and Contributing

  1. Fork repo
  2. Install dev dependencies via yarn
  3. Start test runner/dev mode yarn dev
  4. Add your code and tests if needed - do NOT remove/alter existing tests
  5. Verify that tests pass once minified yarn verify
  6. Commit files
  7. Submit PR with a detailed description of what you're doing
  8. I'll add you to the credits! :)

The Entire Code (for more legibility, see src on GitHub)

const Router = (o = {}) =>
  new Proxy(o, {
    get: (t, k, c) => k === 'handle'
      ? async (r, ...a) => {
          for (let [p, hs] of t.r.filter(i => i[2] === r.method || i[2] === 'ALL')) {
            let m, s, u
            if (m = (u = new URL(r.url)).pathname.match(p)) {
              r.params = m.groups
              r.query = Object.fromEntries(u.searchParams.entries())

              for (let h of hs) {
                if ((s = await h(r, ...a)) !== undefined) return s
              }
            }
          }
        }
      : (p, ...hs) =>
          (t.r = t.r || []).push([
            `^${((t.base || '') + p)
              .replace(/(\/?)\*/g, '($1.*)?')
              .replace(/\/$/, '')
              .replace(/:(\w+)(\?)?(\.)?/g, '$2(?<$1>[^/$3]+)$2$3')
            }\/*$`,
            hs,
            k.toUpperCase(),
          ]) && c
  })

Special Thanks

This repo goes out to my past and present colleagues at Arundo - who have brought me such inspiration, fun, and drive over the last couple years. In particular, the absurd brevity of this code is thanks to a clever [abuse] of Proxy, courtesy of the brilliant @mvasigh. This trick allows methods (e.g. "get", "post") to by defined dynamically by the router as they are requested, drastically reducing boilerplate.

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

  • @technoyes - three kind-of-a-big-deal errors fixed. Imagine the look on my face... thanks man!! :)
  • @hunterloftis - router.handle() method now accepts extra arguments and passed them to route functions
  • @roojay520 - TS interface fixes
  • @mvasigh - proxy hacks courtesy of this chap

Documentation Fixes