ordner v0.5.3
Ordner
A simple file-based router for Polka inspired by Sapper and Svelte Kit.
("Ordner" is German and means "folder")
Note: This is an ES6 module.
Install
npm install ordner
Usage
Create your folder structure:
src/
├─ routes/
│ ├─ users/
│ │ ├─ index.js
│ │ ├─ :id.js
├─ index.js
Import Ordner and call it with the path of your folder containing the routes as the first argument and your Polka instance as the second one. It returns a promise that resolves when all routes have been mounted:
import polka from "polka";
import ordner from "ordner";
const server = polka();
await ordner("./src/routes", server);
server.listen(3000, () => {
console.log(`> Running on localhost:3000`);
});
Note: Make sure to enable ES6 modules project wide by including the following line in your package.json
:
type: "module",
Logging
By default Ordner prints a list of all route handlers and middlewares it found in the specified folder and in the order they are applied to your Polka instance. If you wish to disable logging, e.g. in production
, you can pass in an object as the third argument and and set logging
to false
:
await ordner("./src/routes", server, { logging: false });
Params
To use parameters in your routes simply name your folders and files accordingly. See also Polka's docs.
routes/
├─ blog/
│ ├─ :slug.js
├─ products/
│ ├─ :id.js
├─ users/
│ ├─ :id/
│ │ ├─ index.js
│ │ ├─ orders.js
│ ├─ index.js
Handlers
You export handlers from your .js
files and name them according to their HTTP method in lowercase. Since delete
is a reserved keyword in JavaScript, export a function called del
instead to handle DELETE
requests.
Middlewares
To use middlewares every file can export an array named useBefore
. Middlewares included in this array will be applied before any handler in the current file.
import { json } from "@polka/parse";
export const useBefore = [json()];
To use middlewares before a specific handler you can export arrays named useBeforePost
, useBeforeGet
, useBeforePut
, useBeforePatch
, and useBeforeDel
.
import { json } from "@polka/parse";
export const useBefore = [json()];
export function get(req, res) {
res.end("Hello Ordner!");
}
export const useBeforePut = [(req, res, next) => next()];
export function put(req, res) {
res.end("updated");
}
Hook
You can optionally pass a hook
function to Ordner which is essentially a wrapper around yout handlers. It allows you easily execute additional code before and after each or just a few specific handlers. The hook
function takes the handler
as its only argument and returns a function with the res
and req
signature:
import polka from "polka";
import ordner from "ordner";
const hook = (handler) => async (req, res) => {
res.setHeader("Content-Type", "application/json");
await handler(req, res);
console.log("Request completed.");
};
await ordner("./src/routes", server, { hook });
server.listen(3000, () => {
console.log(`> Running on localhost:3000`);
});
Note 1: You must call handler
inside your hook
function.
Note 2: The hook
is only applied to request handlers not middlewares.
Hook Recipes
- Sending responses like in Svelte Kit
- Obscuring IDs from your database
Sending responses like in Svelte Kit
To handle responses similar to how you would in Svelte Kit, put the following code into your hook
function:
const hook = (handler) => async (req, res) => {
const { status = 200, headers = {}, body } = await handler(req);
res.writeHead(status, {
...headers,
"Content-Type": "application/json",
});
res.end(JSON.stringify(body));
};
Now you can simply return a { status, body, headers }
object from your handlers:
export function get(req) {
return {
body: { message: "Hello Ordner!" },
};
}
Obscuring IDs from your database
Another useful thing you can do with hook
is obscuring IDs from your database. Database IDs are usually sequential numbers. Displaying them in your URLs like /products/17
might tempt your visitors to play around with them in a way you do not want them to. You can use the hook
function to encode all IDs to something that looks more random before sending a response and decode them again when recieving requests.
// helper function to modify objects even if they contain other objects or arrays
function modifyObj(obj, fn) {
Object.keys(obj).forEach((key) => {
const value = obj[key];
if (value !== null && typeof value === "object") {
return modifyObj(value, fn);
}
if (Array.isArray(value)) {
return value.forEach((obj) => modifyObj(obj, fn));
}
fn(key, obj);
});
}
function decode(obj) {
modifyObj(obj, (key, obj) => {
if (key === "id" || key.endsWith("_id")) {
obj[key] = Number(Buffer.from(obj[key], "base64").toString("ascii"));
}
});
}
function encode(obj) {
modifyObj(obj, (key, obj) => {
if (key === "id" || key.endsWith("_id")) {
obj[key] = Buffer.from(String(obj[key])).toString("base64");
}
});
}
const hook = (handler) => async (req, res) => {
// decode the ID from the incoming request
decode(req.params);
// handle the request by the corresponding handler
let { status = 200, headers = {}, body } = await handler(req);
// encode all IDs
encode(body);
// send the response
res.writeHead(status, {
...headers,
"Content-Type": "application/json",
});
res.end(JSON.stringify(body));
};
Create the following route:
/students/:id.js
const students = [
{
id: 173,
first_name: "Tony",
last_name: "Stark",
school_id: 19,
},
];
export function get(req) {
const student = students.find(({ id }) => id === req.params.id);
return {
body: student,
};
}
A GET
request to /students/MTcz
will now return the following response:
{
"id": "MTcz",
"first_name": "Tony",
"last_name": "Stark",
"school_id": "MTk="
}
The above example uses base 64 encoding. You can also use something like Hashids instead, which is more difficult for others to decode.