Mastro 👨‍🍳 Docs

Search results

Blog Community GitHub   Discord   Bluesky

Routing

There are two types of pages in almost any web project:

Unprocessed files

In Mastro, files placed in your project’s routes folder are sent out unmodified to the browser – with two exceptions:

The simplest Mastro website is thus a single routes/index.html file, which will be sent out unprocessed. Other static assets can also be placed there (e.g. routes/favicon.ico).

For example, if you’d like to have a folder called assets, create it, and add a file routes/assets/styles.css, and perhaps one routes/assets/scripts.js. Then reference them like:

routes/index.html
<!doctype html>
<html lang="en">
  <head>
    <title>My page</title>
    <link rel="stylesheet" href="/assets/styles.css">
    <script type="module" src="/assets/scripts.js"></script>
  </head>
  <body>
    <h1>My page</h1>
  </body>
</html>
Copied!

See the Mastro guide for more about CSS, vanilla client-side JavaScript, and Reactive Mastro.

To link to other pages, or to place images on your page, use the standard <a> and <img> elements respectively. Mastro does not use special <Link> or <Image> constructs.

For links (and also for references to unprocessed files), it’s easiest to always use absolute paths that start with a /. For example href="/assets/styles.css" above.

The file-based router (default)

While Mastro also comes with an Express-like programmatic router, the default router is file-based. This means the folder structure determines under what URL a route is served.

Just like unprocessed files, route handlers in Mastro are also placed in the routes folder. But if a file is named *.server.js (or *.server.ts), it’s a route handler (also known as page handler), and contains JavaScript functions to handle the generation of the pages at that route.

Route handlers

A route handler is a function that receives a standard Request object, and returns a standard Response object (or a Promise of such, meaning the function can be async).

Using the default file-based router, this function needs to be exported under the name of a HTTP method. For example for an HTTP GET:

routes/index.server.ts
export const GET = (req: Request) => {
  return new Response("Hello World");
}
Copied!

Since they return a standard Response object, route handlers can be used to generate HTML, JSON, XML, plain text (like the above example), binary data such as images, or whatever else you can think of. If you’re running a server, a Response can also represent a redirect.

Files and folders

Different hosting providers often serve the same file under slightly different urls. In Mastro, the URL for a file does not end with a slash, while the URL for a folder does end with a slash. Since a folder itself cannot contain any code, an index.html or index.server.js file is used to represent the containing folder.

File in routes/ URL
file.html /file.html
folder/index.html /folder/
file.server.js /file
folder/index.server.js /folder/
folder/(folder).server.js /folder/

Since having lots of files called index.server.js would get confusing quickly, you can also name it (folder).server.js, where folder is the name of the containing folder.

Route parameters

Using route parameters, a single route can represent many different pages with the same basic url structure. For example, to match any URL of the form /blog/*/, you could use a route parameter called slug:

routes/blog/[slug].server.ts
import { getParams } from "@mastrojs/mastro";

export const GET = (req: Request) => {
  const { slug } = getParams(req);
  return new Response(`Current URL path is /blog/${slug}`);
}
Copied!

Above, slug is just an example name for the parameter. You can name parameters whatever you want, as long as it’s alphanumeric. (Mastro uses the standard URLPattern API under the hood.)

To capture URL segments containing slashes (often called rest parameters), use [...slug] instead of [slug].

Both of these can be used as the name of a folder inside routes/, or like above, as part of the route handler file name. To debug and console.log your routes, you can import { loadRoutes } from "@mastrojs/mastro".

When you’re using Mastro as a static site generator and have a route with a route parameter, Mastro cannot guess which URL paths you want to generate. In that case, you need to export an additional function getStaticPaths, which needs to return an array of strings (or a Promise of such):

routes/blog/[slug].server.ts
export const getStaticPaths = async () => {
  return ["/blog/my-first-post/", "/blog/we-are-back/"];
}
Copied!

See the guide for an example of a static blog from markdown files.

Programmatic router

As an alternative to the file-based router, Mastro also offers a programmatic router, which is similar to Express.js or Hono.

Modify the server.ts file in your project to something like the following.

server.ts
import { getParams } from "@mastrojs/mastro";
import { Mastro } from "@mastrojs/mastro/server";

const fetchHandler = new Mastro()
  .get("/", () => new Response("Hello world")
  .post("/", () => new Response("Hello HTTP POST")
  .get("/blog/:slug/" => (req) => {
    const { slug } = getParams(req);
    return new Response(`Hello ${slug}`);
  })
  .createHandler();

Deno.serve(fetchHandler);
Copied!

If you’re not using Deno, the last line will look different, but the fetchHandler will also take the place of mastro.fetch from the file-based router.

The first argument of each call (e.g. "/blog/:slug/") is used as the pathname to construct a standard URLPattern. The second argument is a route handler. For more info, see the API docs for the Mastro class.

If you have more than a few routes, it makes sense to place their route handlers in dedicated files and import them (instead of inline, like in the example above). However, with the programmatic router, they don’t need to be in the routes folder.