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.
The idiomatic way of defining routes in Fastify can sometimes have its drawbacks:
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.
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.
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>
));
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>
));
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.
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
.
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