A Native JavaScript Route / Router Proposal
Tags: deno, javascript, node, typescript
Published: 2023-01-23 01:24:02

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](https://wintercg.org/) or [Mozilla](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) would create. Here's a simple Route and Routes class that matches on method and URL.

```ts
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
    }
  }
}
```

This would allow you to do something like:

```ts
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 });
```

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.

```ts
new RouteMatcher(
  method.get,
  oneOf(urlPattern('/elephant'), urlPattern('/elephant/:name')),
  oneOf([queryParam('name'), urlPattern('/elephant/:name'), header('x-name')]),
)
```

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](https://graphql.org/) or [TRPC](https://trpc.io/) 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.