DEV Community

Tea Reggi
Tea Reggi

Posted on • Edited on

A Native JavaScript Route / Router Proposal

The new set of standard classes, such as URLPattern, Response, Request, and URL, has made me ponder what native Route/Router classes would look like. I've experimented with various server frameworks like Express, Oak, and Fresh, and have been contemplating what a server would look like from the perspective a "standard" that an organization like WinterCG or Mozilla would create. Here's a simple Route and Routes class that matches on method and URL.

type Handler = (req: Request) => Promise<Response> | Response

export class Route {
  urlPattern: URLPattern
  constructor (
    public method: string,
    public pathname: string,
    public handler: Handler
  ) {
    this.urlPattern = new URLPattern({ pathname })
  }
  static get (pathname: string, handler: Handler) {
    return new Route("GET", pathname, handler)
  }
  // ...other HTTP methods
}

export class Routes {
  routes: Route[]
  constructor (...routes: Route[]) {
    this.routes = routes
  }
  async match (req: Request): Promise<Response> {
    const matches = this.routes.filter(route => {
      return route.urlPattern.test(req.url) && route.method === req.method
    })
    if (matches.length > 1) { 
      return new Response('internal error, more then one route with the same pathname')
    } else if (matches.length === 0) {
      return new Response('internal error, not found')
    } else {
      const response = await matches[0].handler(req)

      return response
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This would allow you to do something like:

import { serve } from "https://deno.land/std@0.173.0/http/server.ts";

const routes = new Routes(
  Route.get('/elephant', () => Response.json('elephant')),
  Route.get('/monkey', () => Response.json('monkey')),
  Route.get('/racoon', () => Response.json('racoon')),
  Route.get('/bunny', () => Response.json('bunny')),
)

// To listen on port 4242.
serve((request) => {
  return routes.match(request)
}, { port: 4242 });
Enter fullscreen mode Exit fullscreen mode

This has one issue: what if you want to match on other parts of the Request? This can become complicated quickly. Along with the standard URL and method, there are the header, query, body, and should a matcher match on the extracted, parsed body? If so, should the body also be validated within the Request object? To access the body, it is behind a promise. To match the body, you would need to know the type of body (e.g. form, XML, JSON) and then use a tool like zod to validate it. It's not outlined here but I had created the idea of WrapedRequest that would cache the body within the instance of the WrapedRequest so that the promise is resolved once and passed to every Router for matching.

A more complex Route class could have all the matching criteria listed out as an array of options and requirements.

new RouteMatcher(
  method.get,
  oneOf(urlPattern('/elephant'), urlPattern('/elephant/:name')),
  oneOf([queryParam('name'), urlPattern('/elephant/:name'), header('x-name')]),
)
Enter fullscreen mode Exit fullscreen mode

I like the concept of a simple Route/Router that can match against various criteria, and I wonder if others are also contemplating this idea. It certainly simplifies things to only match against the method and URL path.

With the rise of "single endpoint" api's like GraphQL or TRPC I could imagine one might want to only "match" based on the body. Say you have one route handle on { name: "thomas" } and another route handle on { company: "dev.to" } with different response handlers, but the same endpoint pathname. As described above you'd need to resolve the request JSON body promise once and pass it to both matchers. Ideally something like an arbitrary function could be used in the spec so you could use a library like zod to match on the body.

I have been pondering the potential for a native Route/Router class that can match against a variety of criteria. I have been experimenting with different server frameworks and considering the design of a server from various perspectives. I propose that a simple Route and Routes class that can match on method and URL is a good starting point, but there is room for expansion to match on other criteria as well. I am curious to hear if others share this perspective and would like to open a discussion on this topic.

Top comments (2)

Collapse
 
jollytoad profile image
Mark Gibson

Yep, I came up with a bunch of simple functions to do similar, jsr.io/@http/fns, and blog about it here... jollytoad.deno.dev/blog/http_fns

I don't see the need for the classes tbh, you can keep it much simpler with functions.

Collapse
 
donglufei profile image
董路飞
else if (matches.length === 0) {

Enter fullscreen mode Exit fullscreen mode

can be abbreviated

else if (!matches.length) {

Enter fullscreen mode Exit fullscreen mode