Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V4.x - itty-router-extras are coming in house! #148

Closed
wants to merge 120 commits into from
Closed

V4.x - itty-router-extras are coming in house! #148

wants to merge 120 commits into from

Conversation

kwhitley
Copy link
Owner

@kwhitley kwhitley commented Mar 6, 2023

Proposed/TODO

  • add itty-router-extras (most of the API)
    • NOTE: a few items have been dropped for now, and a few of the APIs have shifted slightly.
  • add itty-cors
  • add downstream handler support to existing Response creators to further streamline individual route code
    • error - pass this to the .catch() function
    • json/etc - pass this to a .then() after router.handle to transform unformed data into a Response
  • 100% TypeScript
  • 100% test coverage
  • 100% tree-shakeable for ESM users
  • embed route into request.route for determining route catch from within handlers
  • Complete README rewrite
  • Full documentation site on https://itty.dev

Types

Let me get this out there... types in itty are HARD. First, it's a Proxy, which are not very type-friendly to begin with. Next, we want ALL the flexibility (to serve all the various audiences), and that's where the problems really begin. We could, for instance, pass generics at a router level, to define Request type, additional args, etc. This works great, and a single set of types can serve an entire router.

But what if you want to override just a particular route? For example, use a UserRequest on a route that has a withUser middleware upstream. This no longer works.

Conversely, you can do the opposite, and allow full, per-route customization. It's more verbose to be sure, but it allows you to define what you want, where you want it - the tradeoff being that you have to define them more often.

Ultimately, this is the path I'm leaning towards. There's nothing more frustrating than fighting types - including boilerplate (as much as I obviously hate that). With that in mind, here are the proposed type changes:

Changes

  • IRequest will now be a union on Request (Web Standard), to allow type hinting on the Request object. This is overdue.
  • No meaningful generics will be used at the Router level (for now). I'm holding places for <RequestType, Args> though, as that is the signature I'd ultimately like to pass down, if we can ever achieve that.
  • Adding IRequestStrict, which is just like IRequest, but without the generic traps to allow any undeclared variable. Want to lock down a route? Define a custom Request type based on IRequestStrict instead of IRequest.
  • Each route can use custom Request types and args either via typed params, or generics.
    // standard request
    .get('*', (request, env: Env, ctx: ExecutionContext) => {
      request.url
      env.KV
      ctx.waitUntil
    })
    
    // custom request from handler argument
    .get('*', (request: FooRequest) => {
      request.foo
    })
    
    // custom request from handler argument
    .get<BarRequest>('*', (request) => {
      request.bar
    })
    
    // custom request and args from Route
    
    type Env = {
      KV: string
    }
    
    type CF = [
      env: Env,
      ctx: ExecutionContext
    ]
    
    .get<BarRequest, CF>('*', ({ bar }, env, ctx) => {
      env.KV
      ctx.waitUntil
    })

Usage Example

In the example below, note the lack of manual new Response() or even json(data) creation. By including a single downstream handler at the outermost/root router, we can let routes return Responses, raw data, or even Promises to raw data/Responses (e.g. a request to a database). Downstream, we can wrap everything in a Response if not already formed :)

Update

Rather than include a separate handler (respondWithJSON/respondWithError), I've overloaded the existing function signatures to ignore Requests/Responses. This allows them to be used as global post-route handlers (as well).

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

// also allows minimalist importing from direct files
// import { Router } from 'itty-router/Router'

// fake data
const todos = [
  { id: '1', message: 'Pet the puppy.' },
  { id: '2', message: 'Pet the kitten.' },
]

const router = Router()

router
   // withParams can now be used globally
  .all('*', withParams)
  
  // GET list of todos - notice we can just return the data directly
  .get('/todos', () => todos)
  
  // GET single todo
  .get('/todos/:id', ({ id }) => { 
    const todo = todos.find(t => t.id === id)
    
    return todo || error(404, 'That todo was not found')
  })
  
  // 404 for all else
  .all('*', () => error(404))
  

export default {
  fetch: (request, env, context) => router
                                      .handle(request, env, context)
                                      .then(json)    // if sent raw data, wrap it in a Response
                                      .catch(error)  // send error Response for all uncaught errors
}

@kwhitley
Copy link
Owner Author

Closed with the release of v4 :)

@kwhitley kwhitley closed this May 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in progress I'm working on it... allegedly... PENDING RELEASE Final stages before release! :D PRIORITY Do this first!
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants