Simple, 0deps router built using native api's

Simple, 0deps router built using native api's

denowebnative-webrouter

Usually we use something like express.js or other servers and routers in node (or other runtimes, in my case it was Deno, but it makes no difference for this solution). But does it makes sense in 2025? Kinda of, express is still valid and well known, but nowadays we have some native API's to play with, let's see!

I'm talking about the URLPattern which is now available in majority of JS runtimes. URL patters allows you to create pattern from url string like /user/:id and test it with real url, you can also match other URL properties, like port, domain, protocol and so on. But in this simple example I will show you how to create API router in Deno using these native API's.

export type RouteHandler = (req: Request, params: Record<string, string | undefined>) => Promise<Response> | Response;

export interface Route {
    route: URLPattern,
    method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS',
    handler: RouteHandler,
}

class Router {
    protected routes: Route[] = [];

    constructor(protected apiPrefix: string = '/api') {
        
    }

    handle(req: Request) {
        for (const route of this.routes) {
            if (route.method !== req.method)
                continue;

            const match = route.route.exec(req.url);
            const matchedGroups = match?.pathname.groups || {};

            if (match) {
                return route.handler(req, matchedGroups);
            }
        }

        return new Response('Not found', { status: 404 });
    }

    addRoute(route: Route) {
        this.routes.push(route);
    }

   // syntax sugar with some builder pattern
    get(path: string, handler: RouteHandler) {
        this.addRoute({
            route: new URLPattern({pathname: `${this.apiPrefix}${path}`}),
            method: 'GET',
            handler,
        });
        return this;
    }

    post(path: string, handler: RouteHandler) {
        this.addRoute({
            route: new URLPattern({pathname: `${this.apiPrefix}${path}`}),
            method: 'POST',
            handler,
        });
        return this;
    }

    put(path: string, handler: RouteHandler) {
        this.addRoute({
            route: new URLPattern({pathname: `${this.apiPrefix}${path}`}),
            method: 'PUT',
            handler,
        });
        return this;
    }

    delete(path: string, handler: RouteHandler) {
        this.addRoute({
            route: new URLPattern({pathname: `${this.apiPrefix}${path}`}),
            method: 'DELETE',
            handler,
        });
        return this;
    }
}

as you can see, this in only one class, and all the magic happens in handle method, where we test our URLPattern against url from Request (Request and Response are also part of native WEB platform and are available in all modern JS runtimes).

How you use it? Like this:

export const json = (data: any) => new Response(JSON.stringify(data), {
    headers: {
        'Content-Type': 'application/json',
    },
});

export const router = new Router();

router
    .get('/hello/:name', (_, params) => {
        return json({
            message: `Hello ${params.name}`
        });
    })

I added there small helper json that creates Response with valid content type and to stringify object. That's it! We have our simple router, now, how to run it on Deno? Like this:

Deno.serve({ port: 3000 }, async (req: Request) => {
  return await router.handle(req);
});

That's it! Very simple as you can see, of course this is only very simple version that, to turn it into something more useful and secure you should probably work on:

  • auth with some token-based middleware
  • check domain
  • add CORS support
  • improve error handling
  • probably more!

but it's nice to see how standards are evolving and providing more useful building blocks for devs to reduce third-party libraries usage. Using external libraries is not bad in itself, we do it all the time, but if we have good and proven mechanisms in the runtime environment, we should use them as much as possible for several reasons, such as security or reducing package size by removing unnecessary dependencies. Have fun!