berstend / tiny-request-router

:rocket: Fast, generic and type safe router (match request method and path).
MIT License
202 stars 9 forks source link

Query string support? #57

Closed kwhitley closed 3 years ago

kwhitley commented 4 years ago

Hey @berstend - great little library, and definitely helps fill the gap in DX when writing lightweight APIs with Cloudflare Workers...

Quick question:

How would one access query params, or anything else on the original request (from inside the route handler)? I find first class support for route params, which is great, but that's it!

Example:

import { Router } from 'tiny-request-router'
import { asJSON, handleEvent } from './utils' // self-explanatory

const router = new Router()

router.get('/params/:id?', async (params, ...other) => asJSON({ ...params, ...other })
// for GET /params/foo?q=bar, you receive only { id: 'foo' } as a response...
// nothing else passed to the route handler to extract query params from

addEventListener('fetch', handleEvent(router))

I'd love to see something a bit more robust for future extensions/features, but with route params as THE first class citizen in the function handler, naming collisions are likely to occur. If instead of a handler payload like:

{ 
  param1: 'value',
  params2: 'value,
}

... you did something like:

{ 
  params: { 
    param1: 'value',
    param1: 'value',
  },
  query: {
    whatever: 'value',
  },
  ...anythingElseYouThinkOfLater
}

router.get('/example/:id', async ({ params, query }) => new Response(`Found id of ${params.id} and query of ${JSON.stringify(query)}`)

...you free yourself up to extend later without problems. Conversely, if you want to maintain backwards compatibility for now, you could simply use this more extensible pattern on a second param with affecting anyone already using this. That would allow for the easiest notation for those only concerned with route params, while still granting access to more powerful future features that others may be willing to dig for.

router.get('/example1/:id', async ({ id }) => new Response(`Found id of ${id}`))
router.get('/example2/:id', async ({ id }, { params, query }) => new Response(`Found id of ${id} and query of ${JSON.stringify(query)}`))
jahands commented 4 years ago

I simply added the actual request to get access to query params like this:

import { Router, Method, Params } from 'tiny-request-router'

// Let the router know that handlers are async functions returning a Response
type Handler = (params: Params, request: Request) => Promise<Response>

const router = new Router<Handler>()
router.get('/upload/:name', async (params: Params, request: Request) => upload(params, request))

export async function handle(event: FetchEvent): Promise<Response> {
    let request = event.request
    let url = new URL(request.url)

    const match = router.match(request.method as Method, url.pathname)
    if (match) {
        const response = match.handler(match.params, request)
        return response
    }
    return new Response('not found', { status: 404 })
}

async function upload(params: Params, request: Request): Promise<Response> {
    let url = new URL(request.url)
    return new Response(`Hello ${params.name}, ${url.searchParams.get('bob')}`)
}

Does this functionality really need to be added to the tiny-request-router code?

kwhitley commented 4 years ago

Thanks for getting back to me @jahands! Of course it's not needed, but rather a nice quality of life addition. We tend to accept that route params are a first class citizen of the handler, yet often ask for query params just as much. Having to dig for one but not the other just felt incomplete to me.

Regardless, I've done this in itty router - the whole thing weighs in at a bit over 400 bytes, and for the cost of around, I dunno, 20-25 bytes or so, I was able to embed the query as an object for handler convenience.

as easy as this...
request.query = Object.fromEntries(url.searchParams.entries())

My take was this: Why make each and every handler out there (of which there will be many) have to implement query param extraction, when I can do it at the router level once?

Anyway, keep up the great work, and thanks for the inspiration! πŸ‘ πŸ‘

jahands commented 4 years ago

You're welcome! To clarify, this isn't my project I'm just using it πŸ™‚

Nice project! That's pretty cool, and that's amazing how small the code size is. Since it's so little code to add query support into the router I think it may be worth looking into.

kwhitley commented 4 years ago

Ahhh, good point - then high fives to @berstend for it! :)

Yeah, I just wanted something that was ultra-light to include with CloudFlare Workers, which obv. have crap support for dynamic route matching - that behaved similar to Express. Then we just had fun minimizing it to ridiculous levels, haha.

If you see any tricks we missed that could shave a few chars, feel free to PR!! <3

berstend commented 3 years ago

Handling query params is easy to do with built-in methods (new URL(), etc) and therefore outside the scope of this very focussed project. :-)