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

Galo

Minimalist fast & flexible router.

Getting started

Let’s start with this simple app, to return a “Hello world” response to a GET / HTTP request.

import Router from "galo/mod.ts";

const app = new Router();

app.get("/", () => new Response("Hello world"));

For convenience, you can return a string to create a HTML response:

app.get("/", () => "Hello <strong>world</strong>");

Capture values from the path:

app.get("/:name", ({ name }) => `Hello <strong>${name}</strong>`);

Use a willcard to capture the remaining directories (array stored in the _ property).

app.get("/:name/*", ({ name, _ }) => `Hello ${name} and ${_.join(", ")}`);

The Request instance is stored in the request property:

app.get("/:name", ({ name, request }) => `Hello ${name} from ${request.url}`);

Slashes

Trailing or leading 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

Use a wildcard and the next property to create nested routes:

app.get("/hello/:name/*", ({ name, next }) => {
  return next()
    .path("/morning", () => `Good morning, ${name}`)
    .path("/afternoon", () => `Good afternoon, ${name}`)
    .path("/night", () => `Good night, ${name}`);
});

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));
});

Booleans

Instead of paths, it’s possible to use booleans to match a route:

app.path("/item/:action/:id", ({ action, id, next }) => {
  const item = getItem(id);

  if (!item) {
    return new Response("Not Found", { status: 404 });
  }

  return next()
    .get(action === "view", () => printItem(item))
    .get(action === "edit", () => editForm(item))
    .post(action === "edit", () => editItem(item));
});

Default handler

The default() function allows to specify a default handler:

app
  .get("/", () => "Welcome")
  .get("/about", () => "About me")
  .default(() => new Response("Not Found", { status: 404 }));

It can be used also as a nested route:

app.get("/hello/:name/*", ({ name, next }) => {
  return next()
    .path("/morning", () => `Good morning, ${name}`)
    .path("/afternoon", () => `Good afternoon, ${name}`)
    .path("/night", () => `Good night, ${name}`)
    .default(() => `Hello, ${name}!`);
});

Error handler

Use the catch() function to generate a custom response on error:

app.catch(({ error }) =>
  new Response(`Server error: ${error}`, { status: 500 })
);

Web sockets

The .webSocket() function creates a route to capture a WebSocket connection. You can use the socket property to access to the WebSocket instance:

app.webSocket("/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");
});

Of course, you can create webSockets in sub-routes:

app.path("/:name/*", ({ name, next }) => {
  return next()
    .webSocket("ws", ({ socket }) => {
      socket.onopen = () => console.log(`Hello ${name}`);

      socket.onmessage = (event) => {
        console.log(`Message from ${name}:`, event.data);
        socket.send(`Echo: ${event.data}`);
      };

      socket.onclose = () => console.log(`Bye ${name}!`);
    });
});

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();
      },
    }),
  ));

Static files

Use the staticFiles() router 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");

Middlewares

Middlewares allows to execute code before and after executing the router. You can register new middlewares with the use() function:

app.use(async (request, next) => {
  console.log("Before the router");
  const response = next(request);
  console.log("After the router");
  return response;
});

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 them and mount on the paths /items:

import items from "./routes/items.ts";

const app = new Router();

app.path("/items/*", items);