Galo
Minimalist fast & flexible router.
Getting started
// app.ts
import Router from "galo/mod.ts";
const app = new Router();
app
.get("/", () => new Response("Hello world"))
//For convenience, return a string to create a HTML response:
.get("/hello.html", () => "Hello <strong>world</strong>")
//Return an object or array to create a JSON response:
.get("/hello.json", () => ({ text: "Hello world" }))
//Captured values are passed as properties
.get("/:name", ({ name }) => `Hello <strong>${name}</strong>`)
//Support wilcard to capture the remaining path (array stored in the `_` property)
.get("/example/*", ({ _ }) => `The wildcard content is: ${_.join(", ")}`)
//The `Request` instance is passed as `request` property:
.get("/hello", ({ request }) => `Hello from ${request.url}`);
//Returns the app so you can run it with `deno serve app.ts`
export default app;API:
path(path, callback): Matches requests for a specific path.get(path, callback): Matches GET requests for a specific path.get(callback): Matches GET requests for any path.post(path, callback): Matches POST requests for a specific path.post(callback): Matches POST requests for any path.put(path, callback): Matches PUT requests for a specific path.put(callback): Matches PUT requests for any path.delete(path, callback): Matches DELETE requests for a specific path.delete(callback): Matches DELETE requests for any path.socket(callback): Matches requests with WebSocket connections for any path.socket(path, callback): Matches requests with WebSocket connections for a specific path.sse(callback): Matches requests with Server-Send Events for any path.sse(path, callback): Matches requests with Server-Send Events for a specific path.default(callback): Default handler for unmatched requests.catch(callback): Error handler to capture exceptions.staticFiles(path, root): Serve static files from a folder.
Slashes
Trailing, leading or duplicated slashes are ignored by the router.
// The following routes are equivalent
app.get("/hello/world", () => "Hello world");
app.get("/hello/world/", () => "Hello world");
app.get("hello//world", () => "Hello world");Nested routes
Any route handler can return another router (created with the next function)
to prolong the flow.
//Capture all requests to `/hello/:name` path
app.path("/hello/:name", ({ next, name }) => {
//Return a different response per HTTP method:
return next()
.get(() => `Hello ${name} from GET`)
.post(() => `Hello ${name} from POST`)
.put(() => `Hello ${name} from PUT`)
.delete(() => `Hello ${name} from DELETE`);
});Use a wildcard in the parent route to allow nested routes to match additional path segments:
app.get("/good/*", ({ next }) => {
return next()
.path("/morning", () => "Good morning")
.path("/afternoon", () => "Good afternoon")
.path("/night", () => "Good night");
});The nested routes are useful to create a REST API:
app.path("/item/:id", ({ id, next }) => {
const item = getItem(id);
if (!item) {
return new Response("Not Found", { status: 404 });
}
return next()
.get(() => printItem(item))
.put(() => updateItem(item))
.delete(() => deleteItem(item));
});Nested routes automatically inherit default() and catch() handlers from the
parent router, unless explicitly overriden:
app.path("/static/js/*", ({ next }) => {
return next()
// e.g. "/static/js/other.js" will avoid the implicit default 404 Not Found
.default(() => new Response(null, { status: 204 }))
.get("/bundle.js", () => Deno.readFile("bundle.js"));
});Error handler
Use the catch() function to handle errors and generate a custom response. The
caught error is available in the error property.
app.catch(({ error }) =>
new Response(`Server error: ${error}`, { status: 500 })
);Web sockets
The .socket() function creates a route to capture a WebSocket connection. You
can use the socket property to access to the WebSocket instance:
app.socket("/ws", ({ socket }) => {
socket.onopen = () => console.log("Connection opened");
socket.onmessage = (event) => {
console.log("Message from client:", event.data);
socket.send(`Echo: ${event.data}`);
};
socket.onclose = () => console.log("Connection closed");
});Server-Sent Events
The .sse() function creates a route to return a server-send event stream. Use
an async iterator:
import type { ServerSentEventMessage } from "galo/mod.ts";
app.sse("/sse", async function *(): AsyncGenerator<ServerSentEventMessage> {
console.log("SSE connection established:");
// Simulate sending messages every second
for (let i = 0; i < 1000; i++) {
yield { data: `Message ${i + 1}` };
await wait(1000);
}
});Allowed router returns
Routers can returns different types of data:
Response
Return a Response instance for full control:
app.get("/hello", () => new Response("Hello world"));strings
If a router returns a string it’s converted to a HTML response:
app.get("/hello", () => "Hello world");
// Equivalent to:
app.get("/hello", () =>
new Response("Hello world", {
status: 200,
headers: { "Content-Type": "text/html; charset=utf-8" },
}));Body
Instances of Uint8Array, ReadableStream, Blob, ArrayBuffer,
URLSearchParams, FormData, DataView are used as the body of a Response:
app.get("/hello", () => Uint8Array.fromBase64("PGI+ TURO PC9i Ph"));
// Equivalent to:
app.get(
"/hello",
() => new Response(Uint8Array.fromBase64("PGI+ TURO PC9i Ph")),
);File instances
File instances are converted automatically to a HTTP response:
app.get("/hello", () => new File(["foo"], "foo.txt", { type: "text/plain" }));
// Equivalent to:
app.get("/hello", () =>
new Response("foo", {
status: 200,
headers: {
"Content-Type": "text/plain",
"Content-Length": 3,
"Content-Disposition": `attachment; filename="foo.txt"`,
},
}));Async generators
The simplest way to create a stream response is by returning an async generator:
app.get("/hello", async function* () {
yield "This is a stream\n";
await wait(1000);
yield "This is another message\n";
await wait(1000);
yield "This is the last message\n";
});
// Equivalent to:
app.get("/hello", () =>
new Response(
new ReadableStream({
async start(controller) {
controller.enqueue(
new TextEncoder().encode("This is a stream\n"),
);
await wait(1000);
controller.enqueue(
new TextEncoder().encode("This is another message\n"),
);
await wait(1000);
controller.enqueue(
new TextEncoder().encode("This is the last message\n"),
);
controller.close();
},
}),
));Async generators are also used for server-sent events.
Static files
Use the staticFiles() function to serve files from folders.
const root = Deno.cwd() + "/static";
// Serve all requests.
// Requests that return 404 will be handled by the regular routes.
app.staticFiles("/*", root);
// Serve only requests starting with `/img/`
app.staticFiles("/img/*", root + "/img");Distribute the app in different files
For large apps, you may want to distribute routes in different files. You can
use a Router instances as route handlers. Example:
// routes/items.ts
import { Router } from "galo/mod.ts";
const app = new Router();
app.get("/", listItems);
app.post("/", createItem);
app.get("/:id", returnItem);
export default app;Then, import it and mount on the paths /items:
import items from "./routes/items.ts";
const app = new Router();
app.path("/items/*", items);Manage data
You can pass variables that will be available in the inner routes:
import { Router } from "galo/mod.ts";
const app = new Router({
appName: "My app",
});
app.get("/", ({ appName }) => {
return `Welcome to ${appName}`;
});
export default app;Base path
If your app lives in a subdirectory, you can pass a base path as the second argument:
import { Router } from "galo/mod.ts";
const app = new Router({}, "/path/to/my/app");
app.get("/foo", ({ request }) => {
return request.pathname; // /path/to/my/app/foo
});
export default app;