Skip to content
parallelworksPublic

About

Filesystem based router for express.js

Resources

Stars

3 stars

Watchers

2 watching

Forks

Repository files navigation

fsrouter

Filesystem based router for express.js

Installation

npm install @parallelworks/fsrouter

Usage

import express, { RequestHandler } from 'express'
import { initFsRouting } from '@parallelworks/fsrouter'
import path from 'path'
const port = 3000
const app = express()

const routesPath = path.join(__dirname, '_routes')


export const getFsRouter = async (routesPath: string) => {
  const skipMiddleware: RequestHandler = (req, res, next) => next()
  const router = await initFsRouting({
    ensureAdmin: skipMiddleware,
    ensureAuthenticated: skipMiddleware,
    routesPath: routesPath,
  })
  return router
}

export default async function main() {
  const router = await getFsRouter(routesPath)
  app.use(express.json())
  app.get('/healthzz', (req, res) => res.status(200).send())
  app.use('/api', router)

  app.listen(port, () => {
    console.log(`Server listening on port ${port}`)
  })
}

In the above example, a router is created from a path _routes, and the admin and authenticated middleware are effectively disabled by simply calling next().

In a more realistic example, you can define custom middleware functions that determine if a request is authenticated or if the user is an admin.

Logging

By default, every route is printed when it is mounted, with a warning for each handler of a route that is not an async function. Pass logMounts: false to initFsRouting to print nothing while mounting.

userFacingErrorHandler and defaultErrorHandler always print the errors that they handle with console.error.

Creating a route

Routes are declared by exporting a function with the name of an HTTP verb from within one of these files, e.g.

export const GET: Endpoint = (req, res) => {}

In some cases, you may want to add a custom middleware for a route handler. You can optionally export an array instead and route handlers will be executed in order, for example:

export const POST: Endpoint = [
  (req, res, next) => {
    next()
  },
  (req, res) => {
    res.status(200).send()
  },
]

Special cases

  • If the file is named index.ts then it will be the root route for that path.
  • If you prefix the filename with a : then it will be considered to be an Express URL parameter.
  • Folders will take priority over files, so if you have a /api/index.ts and /api.ts, the index.ts file will be added first; let's make sure we don't do this though!
  • If you need to override the routing system for some reason, files and folders prefixed with _ will be ignored. e.g. _index.ts and _lib/helpers.ts would not be added to the router. Any other file under the routes path is imported, and its exports that are named after an HTTP verb become routes, so keep helpers in a _ folder or outside of the routes path.

Special exports

To make a route require admin access, add the following to the endpoint file:

export const ensureAdmin = true

To make a route public, add the following to the endpoint file:

export const guestAccess = true

A route cannot be both public and admin only, so exporting both is an error.

To restrict a method to users with certain roles, export the roles for each method. The user needs at least one of the roles listed:

export const roles = {
  POST: ['org:admin', 'org:settings'],
}

The roles of the user who made the request come from the rolesResolver that is passed to initFsRouting:

const router = await initFsRouting({
  ensureAdmin,
  ensureAuthenticated,
  routesPath,
  rolesResolver: req => req.user.roles,
})

The router refuses to start when these exports cannot be applied, e.g. when roles or validation are set for a method that the file does not export a handler for.

Validation

When you’re using the @parallelworks/fsrouter package, validation is handled with a special export. The full JSON-schema spec is available in these objects, so we can create some advanced validation if necessary.

export const validation = {
    GET: {
        query: {
            ...
        }
    }
} as const

The schemas of a method go under query and body, any other key is an error.

Where the structure of a query object is a valid JSON Schema, e.g. :

{
      type: 'object',
      properties: {
        name: {
          type: 'string',
          minLength: 1,
          description: 'The name of the resource',
        },
      },
      required: ['name'],
      additionalProperties: false,
}

Exporting this object will make sure that any requests that reach your route handlers are the shape defined in your JSON Schema. Any requests that don't match this shape will receive an error response automatically.

That means the type of the request can be asserted, which is done like this:

export const GET: Endpoint<{}, {}, {}, FromSchema<typeof validation.GET.query>> = (req, res) => {
    ...
}

Since URL params are handled by the filename, you can also assert their type as string with the following:

export const GET: Endpoint<{jid: string}, {}, {}, {}> = (req, res) => {
    ...
}

Releasing

Pull requests are squash-merged, and their titles follow Conventional Commits. From those titles, release-please keeps a release pull request open. Merging the release pull request tags the version and publishes it to npm.

About

Filesystem based router for express.js

Resources

Stars

3 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages