Skip to main content
Deno 2 is finally here 🎉️
Learn more

📮 fsrouter | deno doc deno module release

A file system based router for Deno.

Basic usage

Given a project with the following folder structure:

my-app/
├─ pages/
│  ├─ blog/
│  │  ├─ post.ts
│  │  ├─ index.ts
│  ├─ about.ts
│  ├─ index.ts
├─ mod.ts

Each “route file” must export a FsHandler as its default export:

// my-app/pages/blog/post.ts
export default (req: Request) => {
  return new Response("hello world!");
};

.js files are fine as well:

// my-app/pages/blog/post.js
export default (req) => {
  return new Response("hello world!");
};

As well as .jsx and .tsx files, with jsx runtime modules from whichever source you wish:

// my-app/pages/blog/post.tsx

/** @jsx h */
import { h, renderSSR } from "https://deno.land/x/nano_jsx@v0.0.33/mod.ts";

function App() {
  return (
    <html>
      <head>
        <title>Hello from JSX</title>
      </head>
      <body>
        <h1>Hello world</h1>
      </body>
    </html>
  );
}

export default (_req: Request) => {
  const html = renderSSR(<App />);

  return new Response(html, {
    headers: {
      "content-type": "text/html",
    },
  });
};

Initialize a server by calling fsRouter:

// my-app/mod.ts
import { fsRouter } from "https://deno.land/x/fsrouter@{VERSION}/mod.ts";
import { serve } from "https://deno.land/std@{VERSION}/http/server.ts";

// Use the file system router with base directory 'pages'
// The first argument to fsRouter requires an absolute path
// Paths starting with 'file://' are okay
serve(await fsRouter(import.meta.resolve("./pages")));

Now running:

deno run --allow-read --allow-net my-app/mod.ts

Results in routes being served as follows:

File Route
pages/index.ts /
pages/about.ts /about
pages/blog/index.ts /blog
pages/blog/post.ts /blog/post

An options object can be provided as the second argument to fsRouter. See RouterOptions for details.

Dynamic routes

Dynamic routes are supported using the [slug] syntax. This works for files, folders, or both. For example:

File Matches
pages/blog/[id].ts /blog/123, /blog/first-post
pages/[id1]/[id2].ts /any/route
pages/[fallback].ts /caught-all, /any

Matching slug values are provided as the second argument to FsHandler. Given the files as defined in the table above, the route /any/route will be provided a slug object of the shape { id1: 'any', id2: 'route' }:

// my-app/pages/[id1]/[id2].ts
import { type Slugs } from "https://deno.land/x/fsrouter@{VERSION}/mod.ts";

// req url: /any/route
export default (req: Request, slugs: Slugs) => {
  console.log(slugs.id1); // 'any'
  console.log(slugs.id2); // 'route'

  return new Response("Matched dynamic route!");
};

Typed dynamic routes

Slugs can optionally include a :string or :number postfix to exclusively match strings and numbers respectively. For example:

File Matches
pages/blog/[id:number].ts /blog/123, /blog/45
pages/blog/[id:string].ts /blog/first-post, /blog/second-post

Matches for slugs of type :number will be automatically converted to type number:

// my-app/pages/blog/[id:number].ts
import { type Slugs } from "https://deno.land/x/fsrouter@{VERSION}/mod.ts";

// req url: /blog/123
export default (req: Request, slugs: Slugs) => {
  console.log(typeof slugs.id); // 'number'

  return new Response("Matched dynamic route!");
};

This automatic conversion behaviour can be disabled via RouterOptions.convertToNumber.

Watch mode

During development, you can use Deno’s built-in --watch=<folder> to restart the server on changes. Providing a bare --watch has the caveat of not being able to detect new file additions, since by default Deno will watch only files it can statically discover. By providing a root directory, Deno will be able to detect new file additions as well:

deno run --allow-read --allow-net --watch=pages my-app/mod.ts

Permissions

Using fsrouter requires both --allow-read and --allow-net for the following reasons:

  • --allow-read: fsrouter needs to traverse the filesystem in order to discover handler files
  • --allow-net: fsrouter itself doesn’t actually need network access, but since it’s very likely your script will include using fsrouter in tandem with some sort of file server, you’ll likely need this permission grant

Deno Deploy

This module uses dynamic imports to resolve file names to their respective routes. As Deno Deploy does not support dynamic imports, this module is not Deno Deploy compatible. There are workarounds available that involve generating static manifests using a sort of build-step – in the future, supporting this type of workflow will be considered.