Routes & Guards
Understanding routes
In Jeasx, a file system-based routing system is at the heart of the framework.
All routes are stored in the routes directory of your project and are functions that receive request and reply objects from Fastify as props.
The vanilla Fastify request object exposes the current url (e.g. /json/api/request.json?foo=bar
) via request.url
.
Jeasx adds two additional attributes to the Fastify request object to improve the developer experience:
request.path
gives you the plain path without any query parameters (e.g./json/api/request.json
)request.route
provides the path to the current endpoint handler (e.g./json/[...path]
)
Have a look at the JSON example to see request.url, request.path, request.route
at work.
Routes can be written using JSX, but can also be written in JavaScript and/or TypeScript, so supported extensions are: .js(x)|.ts(x)
A route can return various types of payloads for the client, including HTML (default), JSON, or other formats. If you need to perform asynchronous operations, you can declare your route or imported components as async.
Named routes
The only rule for named routes is to enclose the base filename within brackets. This convention allows you to store components, services and utilities besides your routes without exposing them as endpoints.
Route path | URL |
---|---|
src/routes/[index].jsx | / |
src/routes/a/b/[you-name-it].jsx | /a/b/you-name-it |
src/routes/company/[index].tsx | /company |
src/routes/images/[logo.svg].js | /images/logo.svg |
src/routes/api/posts/[update.json].ts | /api/posts/update.json |
src/routes/api/posts/utils/format.ts | This file is not exposed as endpoint. |
Code example
export default function Welcome({ request, reply }) {
return (
<>
{"<!DOCTYPE html>"}
<html lang="en">
<head>
<base href={`${request.path.endsWith("/") ? request.path : request.path + "/"}`} />
<title>Hello World</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
<>
);
}
Dynamic routes
Dynamic routes are wildcards designed to capture all requests for the current folder and its subfolders. They enable the creation of content with a dynamic URL structure, such as pages retrieved from a CMS. If a named route exists in the same folder as dynamic route, the named route will take precedence. The name for a dynamic route is fixed and must be:[...path](.jsx|.js|.tsx|.ts)
Route path | URL |
---|---|
src/routes/blog/[...path].jsx | /blog/article1 /blog/category/article2 |
Code example
export default async function Product({ request, reply }) {
const segments = request.path.split("/");
const product = await (await fetch(`https://dummyjson.com/product/${segments[1]}`)).json();
if (product.message) {
reply.status(404);
return;
}
return (
<Layout title={product.title} description={product.description}>
<article>
<h1>{product.title}</h1>
<p>{product.description}</p>
</article>
</Layout>
);
}
Guards
Guards enable you to intercept requests and are inherited from the root to the current folder. They are valuable for controlling access to a route. Typically, a guard does not return any payload, allowing the request to be handled by the next defined route. However, if a guard does return a payload, it will be delivered to the client, and no other route will be executed.
Route path | URL |
---|---|
src/routes/blog/[...guard].jsx | The code of the guard is executed before named or dynamic routes in the current folder or below. |
Code example
export default function AuthorizationGuard({ request, reply }) {
const authorization = request.headers["authorization"];
if (
authorization !== `Basic ${Buffer.from("demo:demo").toString("base64")}`
) {
reply.header("WWW-Authenticate", 'Basic realm="Restricted Area');
reply.status(401);
return (
<Layout title="Error 401">
<h1>Error 401</h1>
<p>You are not allowed to view this page!</p>
</Layout>
);
}
}
Additionally, a guard has the capability to return an object, which will serve as props for all routes protected by the guard. This feature enables the creation of routes that are entirely independent from the request object, simplifying component testing.
export default function PropsGuard({ request }) {
const body = request.body || {};
return { message: body["message"] };
}
export default function MessageView({ message }) {
return <p>{message ? message : "No message for you"}</p>;
}
Sharing code between browser and server
While you have the flexibility to share code between the server and browser, this is typically an edge case, as the DOM is not relevant on the server side, and node core modules are not compatible with the browser. To optimize build performance during development by efficiently minimizing unnecessary rebuilds, no rebuild is triggered when shared code changes per default.
For all server side code (e.g. routes), Jeasx watches all files below src
, but ignores all files in src/browser
per default. If you want to import browser code into server code (e.g. when pre-rendering Preact/React components on server side), you can specify advanced 'ignore rules'.
The following rule ignores all changes in src/browser
, but triggers a rebuild of server code when a file changes in src/browser/react
or src/browser/preact
.
JEASX_BUILD_ROUTES_IGNORE_WATCH="src/browser/!(react|preact)"