Skip to content

Migrations

Migration Guides > v4 to v5 - itty-router

v5 is a major step forward in terms of robustness, power, and slimness. Here's a quick rundown of all changes:

Changes in v5

  • breaking router.fetch replaces router.handle to enable cleaner exports.
  • breaking createCors() has been replaced with the improved cors().
  • breaking RouteHandler (type) has been replaced with RequestHandler.
  • change previous Router is now preserved as IttyRouter.
  • added new Router (backwards-compatible) adds support for stages.
  • added new batteries-included AutoRouter adds default settings to Router.
  • change TypeScript support has been improved in all of the routers, allowing router-level generics AND route-level generic overrides in the same router.

1. router.handle is deprecated in favor of router.fetch

At some point in v4, we began to support (at the cost of extra bytes) boththe original router.handle and the newer router.fetch, which matches the default export signature of Cloudflare Workers and Bun. Using router.fetch allows you to simply export the router, allowing its internal .fetch method to match up to what the runtime expects.

To save bytes, we're dropping support for router.handle, preferring router.fetch in all cases.

ts
export default {
  fetch: router.handle, // previously
  fetch: router.fetch, 
}

// or simply 
export default router 

2. CORS: cors() replaces previous createCors()

This was a big one. As some folks have found, the previous CORS solution included a nasty race condition that could affect high-throughput APIs using CORS. The solution required a breaking change anyway, so we ripped off the bandage and rewrote the entire thing. By changing the name as well, we're intentionally drawing attention that things have changed. While this is a non-trivial migration compared to the others, it comes with a series of advantages:

  • It now supports the complete options that express.js does, including support for various formats (e.g. string, array of string, RegExp, etc). We have embraced the industry-standard naming (this is a change).
  • It correctly handles the previously-identified race condition
  • It correctly handles some other edge-cases not previously addressed
  • It handles far-more-complex origin cases & reflection than the previous version
  • We added all this extra power while shaving off 100 bytes. 🔥🔥🔥
ts
import { Router, json, error,
  createCors, 
  cors, 
} from 'itty-router'

const router = Router()
const { preflight, corsify } = createCors() 
const { preflight, corsify } = cors() 

router.get('/', () => 'Success!')

export default {
  fetch: (request, ...args) => 
    router
      .fetch(request, ...args)
      .then(json)
      .catch(error)
      .then(corsify) 
      .then((req, res) => corsify(req, res)) 
}
ts
import { AutoRouter, cors } from 'itty-router'

const { preflight, corsify } = cors() 

const router = AutoRouter({
  before: [preflight], // add preflight to upstream middleware
  finally: [corsify], // and corsify to response handlers
})

router.get('/', () => 'Success!')

export default router

3. The Three Routers

v5 preserves the original Router as IttyRouter, and introduces two advanced routers, Router and AutoRouter. Router is a 100% backwards-compatible swap, with perfect feature-parity with the previous Router.

Here are the key changes:

Added functionality to standard Router

  • Added: before stage - an array of handlers that processes before route-matching
  • Added: catch - a handler that will catch any thrown error. Errors thrown during the finally stage will naturally not be subject to the finally handlers (preventing it from throwing infinitely).
  • Added: finally stage - an array of handlers that processes after route-matching, and after any error thrown during the before stage or route-matching.

Example

ts
import { Router, error, json, withParams } from 'itty-router'

const router = Router({
  before: [withParams], 
  catch: error, 
  finally: [json], 
})

router
  .get('/params/:id', ({ id }) => `Your id is ${id}.`) // withParams already included
  .get('/json', () => [1,2,3]) // auto-formatted as JSON
  .get('/throw', (a) => a.b.c) // safely returns a 500

export default router // CF Workers or Bun

Added AutoRouter

This is a thin wrapper around Router, with a few default behaviors included, and a couple additional options for fine-tuned control.

  • withParams is included by default before the before stage, allowing direct access of route params from the request itself.
  • json response formatter is included by default before the finally stage, formatting any unformatted responses as JSON.
  • A default 404 is included for missed routes. This is equivalent to router.all('*', () => error(404), and may be changed using the missing option (below).
  • Added missing option to replace the default 404. Example { missing: error(404, 'Are you sure about that?') }. To prevent any 404, include a () => {} no-op here.
  • Added format option to replace the default formatter of json. To prevent all formatting, include a () => {} no-op here.

Example

ts
import { AutoRouter } from 'itty-router'

const router = AutoRouter() 

router
  .get('/params/:id', ({ id }) => `Your id is ${id}.`) // withParams already included
  .get('/json', () => [1,2,3]) // auto-formatted as JSON
  .get('/throw', (a) => a.b.c) // safely returns a 500

export default router // CF Workers or Bun

Bigger Example

js
import { AutoRouter, error } from 'itty-router'

// upstream middleware to embed a start time
const withBenchmarking = (request) => {
  request.start = Date.now()
}

// downstream handler to log it all out
const logger = (res, req) => {
  console.log(res.status, req.url, Date.now() - req.start,  'ms')
}

// now let's create the router
const router = AutoRouter({
  port: 3001, 
  before: [withBenchmarking], 
  missing: () => error(404, 'Custom 404 message.'), 
  finally: [logger], 
})

router.get('/', () => 'Success!')

export default router // Bun server on port 3001

4. TypeScript Changes

We've kept things mostly in place from the last round on this, with some improvements, and one minor (technically breaking) change.

Improved router/route generics

We've added the ability to include router-level generics AND route-level generics in the same router. Before, this was not possible, forcing users to pick one or the other.

ts
import { IRequestStrict, Router } from 'itty-router'

type FooRequest = {
  foo: string
} & IRequestStrict

type BarRequest = {
  bar: string
} & IRequestStrict

const router = Router<FooRequest>() // router-level generic

router
  .get('/', (request) => {
    request.foo // foo is valid
    request.bar // but bar is not
  })

  // use a route-level generic to override the router-level one
  .get<BarRequest>('/', (request) => { // route-level override
    request.foo // now foo is not valid
    request.bar // but bar is
  })

Previous RouteHandler becomes RequestHandler breaking

Technically, no one should be affected by this, as RouteHandler was not a documented/advertised type (but rather used internally). Nevertheless, as it was an exported type, we're drawing attention to the change.

To make things more explicit, we've renamed RouteHandler to the more correct RequestHandler. While originally only used in route-definitions, this type is actually the foundation of all handlers/middleware, and thus needed a more appropriate name before people started using it externally.

ts
import { RequestHandler } from 'itty-router'

export const withUser: RequestHandler = (request) => { 
  request.user = 'Kevin'
}