GithubHelp home page GithubHelp logo

httpland / http-router Goto Github PK

View Code? Open in Web Editor NEW
5.0 1.0 0.0 252 KB

HTTP request router for standard Request and Response

Home Page: https://deno.land/x/http_router

License: MIT License

TypeScript 100.00%
http request response router routing nested-routes routing-table handler pattern-matching

http-router's Introduction

http-router

logo

HTTP request router for standard Request and Response.

deno land deno doc GitHub release (latest by date) codecov GitHub

test NPM


Features

  • Based on URL pattern API
  • Web standard API compliant
  • Declarative
  • Functional programing pattern matching style
  • Automatically HEAD request handler
  • Nested route pathname
  • Tiny
  • Universal

Packages

The package supports multiple platforms.

  • deno.land/x - https://deno.land/x/http_router/mod.ts
  • npm - @httpland/http-router

URL router

URLRouter provides routing between URLs and handlers.

It accepts the URLPattern API as is. This means that various url patterns can be matched.

import { URLRouter } from "https://deno.land/x/http_router@$VERSION/mod.ts";
import { serve } from "https://deno.land/std@$VERSION/http/mod.ts";

const handler = URLRouter([
  [{ pathname: "/" }, () => new Response("Home")],
  [
    { password: "admin", pathname: "/admin" },
    (request, context) => new Response("Hello admin!"),
  ],
]);

await serve(handler);

It accepts a set of URLPatternInit and handlers wrapped by Iterable object.

In other words, it is not limited to arrays.

import { URLRouter } from "https://deno.land/x/http_router@$VERSION/mod.ts";

const handler = URLRouter(
  new Map([
    [{ pathname: "/" }, () => new Response("Home")],
  ]),
);

Pathname routes

URLPattern routes are the most expressive, but somewhat verbose. URL pattern matching is usually done using pathname.

URLRouter supports URL pattern matching with pathname as a first class.

import { URLRouter } from "https://deno.land/x/http_router@$VERSION/mod.ts";

const handler = URLRouter({
  "/api/students/:name": (request, context) => {
    const greeting = `Hello! ${context.params.name!}`;
    return new Response(greeting);
  },
  "/api/status": () => new Response("OK"),
});

same as:

import { URLRouter } from "https://deno.land/x/http_router@$VERSION/mod.ts";

const handler = URLRouter(
  [
    [
      { pathname: "/api/students/:name" },
      (request, context) => {
        const greeting = `Hello! ${context.params.name!}`;
        return new Response(greeting);
      },
    ],
    [{ pathname: "/api/status" }, () => new Response("OK")],
  ],
);

URL Route handler context

The URL route handler receives the following context.

Name Description
pattern URLPattern
URL pattern.
result URLPatternResult
Pattern matching result.
params URLPatternResult["pathname"]["groups"]
URL matched parameters. Alias for result.pathname.groups.

URL match pattern

URL patterns can be defined using the URL pattern API.

  • Literal strings which will be matched exactly.
  • Wildcards (/posts/*) that match any character.
  • Named groups (/books/:id) which extract a part of the matched URL.
  • Non-capturing groups (/books{/old}?) which make parts of a pattern optional or be matched multiple times.
  • RegExp groups (/books/(\\d+)) which make arbitrarily complex regex matches with a few limitations.

Check routes validity

The router never throws an error. If the route is invalid, it will be eliminated just.

To make sure that URLRoutes are valid in advance, you can use the validate function.

For example, ? as pathname is an invalid pattern.

import {
  URLRouter,
  URLRoutes,
  validateURLRoutes,
} from "https://deno.land/x/http_router@$VERSION/mod.ts";

const routes: URLRoutes = {
  "?": () => new Response(),
};
const result = validateURLRoutes(routes);

if (result !== true) {
  // do something
}

const handler = URLRouter(routes);

The validate function returns true in case of success, or an object representing the contents of the Error in case of failure.

Invalid route means the following:

  • Invalid URLPattern
  • Duplicate URLPattern

You are completely free to do this or not.

Nested route pathname

nest is nested URL pathname convertor. It provides a hierarchy of routing tables.

Hierarchical definitions are converted to flat definitions.

You can define a tree structure with a depth of 1. To nest more, combine it.

Example of a routing table matching the following URL:

  • /
  • /api/v1/users
  • /api/v1/products
  • /api/v2/users
  • /api/v2/products
import {
  nest,
  URLRouter,
} from "https://deno.land/x/http_router@$VERSION/mod.ts";

const routeHandler = () => new Response();
const v2 = nest("v2", {
  users: routeHandler,
  products: routeHandler,
});
const api = nest("/api", {
  ...nest("v1", {
    users: routeHandler,
    products: routeHandler,
  }),
  ...v2,
});
const handler = URLRouter({ ...api, "/": routeHandler });

Concatenate path segment

Path segments are concatenated with slashes.

import { nest } from "https://deno.land/x/http_router@$VERSION/mod.ts";
import { assertEquals } from "https://deno.land/std@$VERSION/testing/asserts.ts";

const routeHandler = () => new Response();
assertEquals(
  nest("/api", {
    "/hello": routeHandler,
    "status/": routeHandler,
  }),
  {
    "/api/hello": routeHandler,
    "/api/status/": routeHandler,
  },
);

Ambiguous pattern

The routing table defined in nest may have duplicate url patterns in some cases.

As seen in Concatenate path segment, segment slashes are safely handled. This results in the following definitions being identical

  • branch
  • /branch

These are converted to the following pathname:

[root]/branch

In this case, the routing table is ambiguous.

Route with the same pattern always take precedence first declared route.

This is because pattern matching is done from top to bottom.

Pattern matching performance

Pattern matching is done from top to bottom. The computational complexity is usually O(n).

Pattern matching is done on URLs, so they are safely cached.

Already matched URL patterns have O(1) complexity.

HTTP request method router

MethodRouter provides routing between HTTP request methods and handlers.

import { MethodRouter } from "https://deno.land/x/http_router@$VERSION/mod.ts";
import { serve } from "https://deno.land/std@$VERSION/http/mod.ts";

const handler = MethodRouter({
  GET: () => new Response("From GET"),
  POST: async (request) => {
    const data = await request.json();
    return new Response("Received data!");
  },
});

await serve(handler);

HEAD request handler

By default, if a GET request handler is defined, a HEAD request handler is automatically added.

This feature is based on RFC 9110, 9.1

All general-purpose servers MUST support the methods GET and HEAD.

import { MethodRouter } from "https://deno.land/x/http_router@$VERSION/mod.ts";
import { serve } from "https://deno.land/std@$VERSION/http/mod.ts";
import { assertEquals } from "https://deno.land/std@$VERSION/testing/asserts.ts";

const handler = MethodRouter({
  GET: () => {
    const body = `Hello! world`;
    return new Response(body, {
      headers: {
        "content-length": new Blob([body]).size.toString(),
      },
    });
  },
});
const request = new Request("http://localhost", { method: "HEAD" });
const response = await handler(request);

assertEquals(response.body, null);
assertEquals(response.headers.get("content-length"), "12");

This can be disabled by setting withHead to false.

import { MethodRouter } from "https://deno.land/x/http_router@$VERSION/mod.ts";

const handler = MethodRouter({}, { withHead: false });

Hook on matched handler

The router provides hooks for cross-cutting interests.

Before each

Provides a hook to be called before the handler is invoked.

You can skip the actual handler call on a particular request by passing a Response object.

The handler call is skipped and the afterEach hook described below is called.

Example of handling a preflight request that is of transversal interest:

import { URLRouter } from "https://deno.land/x/http_router@$VERSION/mod.ts";
import { assertEquals } from "https://deno.land/std@$VERSION/testing/asserts.ts";
import { preflightResponse } from "https://deno.land/x/cors_protocol@$VERSION/mod.ts";

const handler = URLRouter({
  "/": () => new Response(),
}, {
  beforeEach: (request) => {
    const preflightRes = preflightResponse(request, {});
    return preflightRes;
  },
});

After each

Provides a hook that is called after each matching handler is called.

With this hook, you can monitor the handler's call and modify the resulting response.

To modify the response, a response object must be returned to the hook.

import { URLRouter } from "https://deno.land/x/http_router@$VERSION/mod.ts";
import { assertEquals } from "https://deno.land/std@$VERSION/testing/asserts.ts";

const handler = URLRouter({
  "/": () => new Response(),
}, {
  afterEach: (response) => {
    response.headers.set("x-router", "http-router");
    return response;
  },
});

assertEquals(
  (await handler(new Request("http://localhost"))).headers.get("x-router"),
  "http-router",
);
assertEquals(
  (await handler(
    new Request("http://localhost/unknown"),
  )).headers.get("x-router"),
  null,
);

Detect error in router

If your defined handler throws an error internally, it will be supplemented and safely return a Response.

Here is the default response on error.

HTTP/1.1 500 Internal Server Error

onError is called when an error is thrown internally by the handler. You may customize the error response.

import { URLRouter } from "https://deno.land/x/http_router@$VERSION/mod.ts";

const handler = URLRouter({
  "/": () => {
    throw Error("oops");
  },
}, {
  onError: (error) => {
    console.error(error);
    return new Response("Something wrong :(", {
      status: 500,
    });
  },
});

Spec

In addition to user-defined responses, routers may return the following responses:

Status Headers Condition
404 URLRouter
If not all url pattern match.
405 allow MethodRouter
If HTTP method handler is not defined.
500 URLRouter, MethodRouter
If an internal error occurs.

API

All APIs can be found in the deno doc.

Benchmark

Benchmark script with comparison to several popular routers is available.

deno task bench

Benchmark results can be found here.

Related

More detailed references:

Recipes

URLRouter + MethodRouter

URLRouter and MethodRouter are independent, but will often be used together.

import {
  MethodRouter as $,
  URLRouter,
  URLRoutes,
} from "https://deno.land/x/http_router@$VERSION/mod.ts";

const routeHandler = () => new Response();
const routes: URLRoutes = {
  "/": $({
    GET: routeHandler,
  }),
  "/api/status/?": routeHandler,
  "/api/users/:id/?": (request, { params }) => {
    // params.id!
    return $({
      POST: routeHandler,
    })(request);
  },
};
const handler = URLRouter(routes);

Others

License

Copyright © 2022-present httpland.

Released under the MIT license

http-router's People

Contributors

semantic-release-bot avatar tomokimiyauci avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

http-router's Issues

Clarify the error pattern

Currently, errors are only thrown when the routing table is invalid.

Routing table inspection helps prevent user error and detect incorrect values and duplicate patterns.

However, we would like to know more details about the conditions under which errors are thrown.

Expected

Clarify the conditions for errors and document the situation.

Add stateful router matched on HTTP request method and path

The current router has synthesizability problems because it wraps in an immediate function. In particular, nested routers are difficult to see.

A stateful object-based router improves composability and simplifies nesting.

Add a stateful Router that declares an HTTP method, a URL path, and registers a handler.

Debug mode

Provides a way to see details of internal errors.
This could be useful during development.

URL routing other than pathname

Routing based on URL pathname is the core routing functionality.

However, it would be useful to be able to route based on password or hash instead of just pathname.

Expected

Provides all pattern matching supported by the URLPattern API.

browser compatible

Use only the Universal API and make it executable in the browser.
This allows for use primarily with Web Worker.

Simplify the router

To compose handlers, routing elements should be divisible.

The element can be split for each property of the HTTP request (Request object).

Users will often want to route two elements of an HTTP request:

  • HTTP method
  • Request URL

Expected

Split the routing functionality and provide each function as an independent router.

Remove RouterError

Currently, errors are wrapped in RouterError, but RouterError does nothing.

Middlewares Pattern

I liked these Routing Library & I am now using it with My Supabase Edge Functions, & it's just a great library.

One, thing that is cool to have is middlewares pattern, to Add CORS Header, Check for Authorization Headers & more... before & after Handling Routes.

For Example, The Pattern can be like these :-

import { createRouter } from "https://deno.land/x/http_router/mod.ts";
import { serve } from "https://deno.land/std/http/mod.ts";
import cors from 'https://deno.land/x/edge_cors/src/cors.ts';

const router = createRouter({
  "/*": async (request, { next }) => {
    const response = await next(); // invokes the next route

    return cors(request, response); // set CORS Headers to the response
  },
  "/hello": (request, _context) => {
    return new Response.json({ hello: "world" });
  }
});

await serve(router);

I think these can make a whole lot of use cases possible. Thanks.

Define term

Define the terminology within project.

The target terms are follows:

  • Handler
  • Router

change in error handling policy

Stop griping about errors.
Also, if a handler throws an error, it will throw the error as is instead of returning a 500 error response.

Nested route

The current routing table only accepts flat URL patterns.

Since URLs represent hierarchies, routing tables should also be able to represent hierarchies.

However, this should be a pure syntax sugar for flat URL pattern.
Therefore, nested URL pattern will be flat at initialization time.

Expected:

import { createRouter } from "https://deno.land/x/http_router/mod.ts";
createRouter({
  "/api": {
    "/status": () => new Response("OK"),
    "/hello": {
      GET: () => new Response("world"),
    },
  },
"/api/test": () => new Response("test")
});

concern

  • Duplicate URL pattern
  • Decreased readability

Duplicate URL pattern

No duplicate URL patterns occurred when keying the flat URL pattern.

However, there is a possibility that the flat URL pattern and the nested URL pattern could be identical.

If identical URL patterns are detected, an error is thrown early.

import { createRouter } from "https://deno.land/x/http_router/mod.ts";

createRouter({
  "/api": {
    "/hello": () => new Response(null),
  },
  "/api/hello": () => new Response(null)
}); // throw Error

Decreased readability

Readability concerns remain when nested URLs and method handlers are mixed.

import { createRouter } from "https://deno.land/x/http_router/mod.ts";

createRouter({
  "/api": {
    GET: () => new Response(null),

    "/hello": () => new Response(null),
  },
});
  • GET /api
  • ALL /api/hello

Also, the catch all method cannot be expressed.

import { createRouter } from "https://deno.land/x/http_router/mod.ts";

createRouter({
  "/api": {
    "ALL?": () => new Response(null),

    "/hello": () => new Response(null),
  },
});

Need to handle special keys such as the ALL key.

add `afterEach` hooks for customize handler response

afterEach provides a hook that is called after each handler call.

It can take the actual handler return value and return a Response object.

That is, you can define a custom response.

This hook is only called if a user-defined handler is called. That is, it will not be called:

import { URLRouter } from "https://deno.land/x/http_router/mod.ts"

URLRouter({
  "/api": () => new Response()
}, {
  afterEach: (res) => res
})(new Request("http://localhost/"))

routers should not throw errors

Throwing errors destroys the system.

Instead of throwing errors, here are some ways to express errors:

  • Union of type null
  • Union of error types
  • Result Monad

Neither of these is appropriate.

For example, a result monad might look like this:

const handler = URLRouter({
  "/": MethodRouter({
    GET: () => new Response(),
  }).mapErr(reportError).unwrap(),

  "/api": MethodRouter({
    POST: () => new Response(),
  }).mapErr(reportError).unwrap(),
}).mapErr(reportError).unwrap();

This is terrible.

To begin with, this project is intended to allow routes to be written in an aggregate, complete manner.

Any other proposal would make routes difficult to read, requiring optional operators, etc.

Proposal

This proposal solves this by delegating error checking to another function.
The router will just ignore the route, even if there is an error in the route.
The router will just return a handler, silently rejecting all errors.

Instead, it provides an assert function to verify the error.

The user can choose to use the assert function.

Delete `MethodRouter` and `URLRouter`

These are difficult to compose because they are just handler builders. These were failures.

Delete all related APIs once.
If they are provided again, we would like to incorporate middleware mechanisms in the APIs and provide a functional-like syntax that allows composition between handlers.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.