Filesystem based router for express.js
npm install @parallelworks/fsrouterimport 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.
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.
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()
},
]- If the file is named
index.tsthen 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.tsand/api.ts, theindex.tsfile 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.tsand_lib/helpers.tswould 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.
To make a route require admin access, add the following to the endpoint file:
export const ensureAdmin = trueTo make a route public, add the following to the endpoint file:
export const guestAccess = trueA 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.
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 constThe 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) => {
...
}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.