Routing

Routing is the process of determining how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (GET, POST, and so on). Each route can have one or more handler functions, which are executed when the route is matched.

In the case of Velocity, we are using Fastify, a web framework for Node.js, designed for building web applications and APIs quickly. It provides an extensive routing API, allowing you to define routes for your application and handle client requests effectively.

We adhere to Fastify standards as much as possible and only deviate if we see significant benefits. We are not attempting to build our own framework; instead, we aim to stay compatible with the Fastify ecosystem. One such deviation, which provides significant benefits, is our custom page helper for generating routes and links.

Generating Routes with the Page Helper

The idiomatic way of defining routes in Fastify can sometimes have its drawbacks:

  1. The route parameters within the URL string (e.g. /product/:id) don't affect the TypeScript typings. request.params will always be unknown. It would be possible to mark it as any or to add validation logic in each handler, but we provide a page helper that generates a standard Fastify route definition for both 'GET' and 'POST' methods, providing a simpler way using the combination of template literals and TypeScript.

  2. Web Applications not only define routes but also link to them. This sometimes leads to the situation that there is a mismatch between the path of the registered route and the generated links. Therefore the page utility also generates a link generator that uses the provided URL and ensures with its typings that the right parameters are provided.

You can find the page utility under src/utilities/page.ts in the shop package.

Registering and Linking Pages with the Page Helper

To better understand the use of our custom page helper in Velocity, let's examine a simple example where we register two pages: HelloWorldPage and HomePage.

import page from '~/utilities/page';

export const HelloWorldPage = page`/hello/${'username'}`((request) => (
  <html>
    <body>Hello {request.params.username}</body>
  </html>
));

export const HomePage = page`/`(() => (
  <html>
    <body>
      If your name is Joe: <a href={HelloWorldPage.link({ username: 'Joe' })}>Click here</a>
    </body>
  </html>
));

Registering a Page

We begin by importing the page utility, then we use it to define the HelloWorldPage. This page expects a route parameter username. In the body of the function, we return the HTML that should be displayed when this page is accessed. Note the usage of JavaScript's template literals (the backtick strings), which enable us to embed expressions (like ${'username'}) inside the string. This syntax gives us a powerful and flexible way to define routes.

The function provided as the second argument to the page helper will receive a request object, where request.params will have a username property thanks to the page helper.

export const HelloWorldPage = page`/hello/${'username'}`((request) => (
  <html>
    <body>Hello {request.params.username}</body>
  </html>
));

Linking to a Page

Next, we define the HomePage, which doesn't expect any route parameters. Inside this page's function, we use the HelloWorldPage.link method to generate a URL to the HelloWorldPage. This method takes an object where the keys should match the names of the route parameters.

export const HomePage = page`/`(() => (
  <html>
    <body>
      If your name is Joe: <a href={HelloWorldPage.link({ username: 'Joe' })}>Click here</a>
    </body>
  </html>
));

Here, we generate a link to /hello/Joe, which corresponds to the HelloWorldPage with the username parameter set to 'Joe'.

By using the page helper in this way, we ensure type safety when handling route parameters, and maintain consistency between registered routes and generated links, leading to a more reliable and maintainable application.

Naming conventions

You can see from the above examples that there is a convention on how to name the result from the page utility. The result is named with a Page suffix. Filenames for files which contain page definitions should be *.page.tsx.

There's a special convention for pages that return Hotwire Turbo streams, which will be introduced in a later chapter. Those are called Mutations and the suffix is Mutation instead of Page.

Registering Routes

It is important to note that the page utility doesn't automatically register the routes. Instead, it generates standard Fastify route definitions that you have to register manually. To register these pages, you need to add their route definitions to the root index file, located in src/index.ts in the shop package.

Here's how to do it:

import { RouteOptions } from 'fastify';
import { HelloWorldPage, HomePage } from './pages/demo.page';

export const routes: RouteOptions[] = [HelloWorldPage.route, HomePage.route];

The routes generated by page will be registered for both 'GET' and 'POST' methods. This construct is not limited to routes generated with the page utility. Any definition that complies with Fastify's RouteOption interface can be registered. This allows for adding endpoints that don't adhere to page's limitations, providing a more flexible and maintainable architecture for your application.

Powered by Doctave