Route structure 

Each HTTP route in Gadget has two parts: a URL pattern and a handler function.

URL patterns 

The URL pattern for a route in Gadget is defined by the route file's path within the routes/ directory. The name of the file should start with an HTTP verb, then have a dash, then have a valid URL segment. Requests made with that HTTP verb and segment will invoke the route handler in that file.

For example, if your gadget app is my-app.gadget.app and you have a source file at routes/test/GET-data.js, someone accessing https://my-app.gadget.app/test/data in the browser would run the handler in that source file.

Example filename patterns 

Route filenameMatched requests
api/routes/GET.jsGET /
api/routes/GET-foo.jsGET /foo or GET /foo/, but not GET /bar or POST /foo
api/routes/GET-[id].jsGET /foo, GET /bar, GET /1 but not POST /foo or GET /foo/bar
api/routes/blogs/GET-[id].jsGET /blogs/1, GET /blogs/welcome-to-gadget
api/routes/blogs/POST.jsPOST /blogs, but not POST /blogs/1
api/routes/category/[category]/blogs/GET-[id].jsGET /category/cooking/blogs/1 or GET /category/baking/blogs/5
api/routes/repos/GET-[...].jsGET /repos/foo or GET /repos/foo/bar/baz

Dynamic segments 

If you have a segment in your URL that isn't static, such as an id to or a slug, you can capture a variable by adding a segment wrapped in square brackets. For example, routes/blog/post/GET-[id].js would capture id=5 when accessing /blog/post/5. You can then access these captured segments in request.params when writing a route handler.

api/routes/quotes/GET-[id].js
JavaScript
1import type { RouteHandler } from "gadget-server";
2
3const route: RouteHandler<{ Params: { id: string } }> = async ({
4 request,
5 reply,
6}) => {
7 if (request.params.id == "1") {
8 await reply.send("To be or not to be");
9 } else {
10 await reply.code(404).send("Unknown quote id: " + request.params.id);
11 }
12};
13
14export default route;
1import type { RouteHandler } from "gadget-server";
2
3const route: RouteHandler<{ Params: { id: string } }> = async ({
4 request,
5 reply,
6}) => {
7 if (request.params.id == "1") {
8 await reply.send("To be or not to be");
9 } else {
10 await reply.code(404).send("Unknown quote id: " + request.params.id);
11 }
12};
13
14export default route;

You can also capture part of a path segment, as long as it's separated by a dash. For example, routes/blog/[id]/GET-post-[postId] would capture id=3, postId=10 when accessing /blog/3/post-10.

If you need more flexibility in how parts of the path are captured, you can manually register a route through a route plugin.

All JS/TS files in the routes/ directory must be prefixed with an HTTP verb for route handlers, or prefixed with a + for route plugins. Any other filename under the routes/ directory will throw an error.

Wildcard segments 

If you have a portion of your URL that should match any valid URL segment, including slashes, you can capture it with a wildcard segment using [...]. For example, routes/blog/post/GET-[...].js will match any request to /blog/post/foo, or /blog/post/foo/bar, or any longer segment as well. Wildcards are useful for catchall redirects, 404 pages, or routing schemes that include slashes. You can access the captured wildcard value in request.params['*'] when writing a route handler.

api/routes/blog/post/GET-[...].js
JavaScript
1import type { RouteHandler } from "gadget-server";
2
3const route: RouteHandler<{ Params: { "*": string } }> = async ({
4 request,
5 reply,
6}) => {
7 if (request.params["*"] == "foo/bar") {
8 await reply.send("the foobar post");
9 } else {
10 await reply.code(404).send("Unknown post URL: " + request.params["*"]);
11 }
12};
13
14export default route;
1import type { RouteHandler } from "gadget-server";
2
3const route: RouteHandler<{ Params: { "*": string } }> = async ({
4 request,
5 reply,
6}) => {
7 if (request.params["*"] == "foo/bar") {
8 await reply.send("the foobar post");
9 } else {
10 await reply.code(404).send("Unknown post URL: " + request.params["*"]);
11 }
12};
13
14export default route;

Root URLs 

If you want to register a route for the root of your application, or a route at the root of a folder, name the file after just the HTTP verb. For example, routes/GET.js will match requests to /, or routes/foo/bar/POST.js will match routes to /foo/bar.

Available HTTP methods 

Route handler functions can listen to any of the standard HTTP methods (also known as verbs) by using that HTTP method as the first part of the file name:

  • GET, like GET-list.js
  • POST, like POST-create.js
  • PUT, like PUT-update.js
  • PATCH, like PATCH-update.js
  • DELETE
  • HEAD
  • OPTIONS

Route handlers can also be prefixed with ANY to respond to any HTTP method, like ANY-data.js. To know which HTTP method was used to make a request, you can utilize the request.method property.

api/routes/ANY-example.js
JavaScript
1import type { RouteHandler } from "gadget-server";
2
3const route: RouteHandler = async ({ request, reply }) => {
4 if (request.method == "GET") {
5 await reply.send("hello world");
6 } else if (request.method == "POST") {
7 await reply.send("updated: " + request.body);
8 } else {
9 await reply.code(404).send("not found");
10 }
11};
12
13export default route;
1import type { RouteHandler } from "gadget-server";
2
3const route: RouteHandler = async ({ request, reply }) => {
4 if (request.method == "GET") {
5 await reply.send("hello world");
6 } else if (request.method == "POST") {
7 await reply.send("updated: " + request.body);
8 } else {
9 await reply.code(404).send("not found");
10 }
11};
12
13export default route;

Trailing slashes 

Routes defined in the routes/ folder respond to URLs with or without trailing slashes.

For example, if you define a route at routes/foo/GET-bar.js, requests made to /foo/bar or /foo/bar/ will invoke this route handler.

Route handlers 

Route handlers should export a single function that takes a request and a reply parameter.

Send a plain text response
JavaScript
1import type { RouteHandler } from "gadget-server";
2
3const route: RouteHandler = async ({ request, reply }) => {
4 await reply.send("hello world");
5};
6
7export default route;
1import type { RouteHandler } from "gadget-server";
2
3const route: RouteHandler = async ({ request, reply }) => {
4 await reply.send("hello world");
5};
6
7export default route;
Send a JSON response
JavaScript
1import type { RouteHandler } from "gadget-server";
2
3const route: RouteHandler = async ({ request, reply }) => {
4 await reply.send({ message: "hello world" });
5};
6
7export default route;
1import type { RouteHandler } from "gadget-server";
2
3const route: RouteHandler = async ({ request, reply }) => {
4 await reply.send({ message: "hello world" });
5};
6
7export default route;
JavaScript
1// Plain text response with custom status code
2import type { RouteHandler } from "gadget-server";
3
4const route: RouteHandler = async ({ request, reply }) => {
5 await reply
6 .code(418)
7 .type("text/plain; charset=utf-8")
8 .send("I can't brew you any coffee because I'm a teapot!");
9};
10
11export default route;
1// Plain text response with custom status code
2import type { RouteHandler } from "gadget-server";
3
4const route: RouteHandler = async ({ request, reply }) => {
5 await reply
6 .code(418)
7 .type("text/plain; charset=utf-8")
8 .send("I can't brew you any coffee because I'm a teapot!");
9};
10
11export default route;
api/routes/quotes/GET-[id].js
JavaScript
1// Accessing a captured id argument
2import type { RouteHandler } from "gadget-server";
3
4const route: RouteHandler<{ Params: { id: string } }> = async ({
5 request,
6 reply,
7}) => {
8 if (request.params.id == "1") {
9 await reply.send("To be or not to be");
10 } else {
11 await reply.code(404).send("Unknown quote id: " + request.params.id);
12 }
13};
14
15export default route;
1// Accessing a captured id argument
2import type { RouteHandler } from "gadget-server";
3
4const route: RouteHandler<{ Params: { id: string } }> = async ({
5 request,
6 reply,
7}) => {
8 if (request.params.id == "1") {
9 await reply.send("To be or not to be");
10 } else {
11 await reply.code(404).send("Unknown quote id: " + request.params.id);
12 }
13};
14
15export default route;

For more details on route handlers, see Route configuration.

Route plugins 

Route plugins are files that customize your app for the associated routes. You add a route plugin by creating a file starting with a + sign in a particular folder of routes, to which then modifies the Fastify server powering your app for all the routes in that folder (or subfolders of it). Route plugins in one folder don't modify routes in sibling folders -- only that folder and its subfolders.

Route plugins can do many different things:

  • Adding Fastify plugins like fastify-cors, fastify-view, ...
  • Decorate requests with a logged-in user for authentication
  • Redirect anonymous users to a login page
  • Checking user credentials for permissions to access a set of routes
  • Conditionally registering routes in loops or if statements with server.get, ...

Route plugins are a function that looks like a regular Fastify plugin. Plugin files should export a single function that receives a Fastify server instance as an argument.

Register point-of-view for HTML rendering
JavaScript
1// in routes/+views.ts
2import FastifyView from "point-of-view";
3import type { Server } from "gadget-server";
4
5export default async function (server: Server) {
6 await server.register(FastifyView, {
7 engine: {
8 // an example view engine, see https://eta.js.org/
9 eta: require("eta"),
10 },
11 });
12}
1// in routes/+views.ts
2import FastifyView from "point-of-view";
3import type { Server } from "gadget-server";
4
5export default async function (server: Server) {
6 await server.register(FastifyView, {
7 engine: {
8 // an example view engine, see https://eta.js.org/
9 eta: require("eta"),
10 },
11 });
12}
Redirect when missing a header
JavaScript
1// in routes/+redirect.ts
2import type { Server } from "gadget-server";
3
4export default async function (server: Server) {
5 server.addHook("preHandler", async ({ request, reply }) => {
6 if (request.headers["secret-value"] != "12345") {
7 await reply.redirect("/index");
8 }
9 });
10}
1// in routes/+redirect.ts
2import type { Server } from "gadget-server";
3
4export default async function (server: Server) {
5 server.addHook("preHandler", async ({ request, reply }) => {
6 if (request.headers["secret-value"] != "12345") {
7 await reply.redirect("/index");
8 }
9 });
10}
Register a route with a regex path
JavaScript
1// in routes/+custom-routes.ts
2import type { Server } from "gadget-server";
3
4export default async function (server: Server) {
5 server.get("/example/:file(^d+).png", async ({ request, reply }) => {
6 await reply.send({ filenameWithoutExtension: request.params.file });
7 });
8}
1// in routes/+custom-routes.ts
2import type { Server } from "gadget-server";
3
4export default async function (server: Server) {
5 server.get("/example/:file(^d+).png", async ({ request, reply }) => {
6 await reply.send({ filenameWithoutExtension: request.params.file });
7 });
8}

What routes do route plugins affect 

Route plugins are applied to all routes in the same folder as the plugin, and all subfolders of that folder. For example, if you have a plugin in routes/admin/+auth.js, it will affect all routes in routes/admin and all subfolders of routes/admin, such as routes/admin/users and routes/admin/posts. It will not affect routes in other folders, such as routes/public or routes/blog.

1routes/
2 +plugins.js // will affect all routes in any folder
3 admin/
4 +auth.js // will only affect routes in the admin folder
5 GET.js
6 GET-posts.js
7 users/
8 +customizations.js // will only affect the one route below, and nothing in the routes/admin folder
9 GET.js
10 posts/
11 +images.js // will only affect these posts routes in routes/posts
12 GET.js
13 GET-[id].js

Boot plugins 

Boot plugins are server customizations that affect the whole server instead of just a part. They are registered before any other route plugins or the routes themselves. Boot plugins should live in the root api/boot folder (otherwise they will not work as expected). The primary use case is to register some global state, such as creating an API client of an external service. It isn't necessary to export anything from a boot plugin, but the only export supported is a function that will take a server instance.

Handle errors and missing routes
JavaScript
1// in api/boot/errors.ts
2import type { Server } from "gadget-server";
3
4export default async function (server: Server) {
5 server.setNotFoundHandler(async ({ request, reply }) => {
6 await reply.send(`couldn't find ${request.path}`);
7 });
8 server.setErrorHandler(async (error, request, reply) => {
9 await reply.send(`something failed: ${error.message}`);
10 });
11}
1// in api/boot/errors.ts
2import type { Server } from "gadget-server";
3
4export default async function (server: Server) {
5 server.setNotFoundHandler(async ({ request, reply }) => {
6 await reply.send(`couldn't find ${request.path}`);
7 });
8 server.setErrorHandler(async (error, request, reply) => {
9 await reply.send(`something failed: ${error.message}`);
10 });
11}

Boot plugins can also be used for setting up other global server-side objects, like error handlers or persistent connections to third-party services.

Deprecated route filename syntax 

Gadget supported a previous version of the route filename syntax that isn't compatible with Windows users as it uses filenames with illegal characters on Windows. This route syntax still works, but we recommend moving to the new square-bracket based syntax.

Each old route filename has a corresponding new syntax:

NameOld syntaxNew syntaxOld exampleNew Example
Single dynamic segment:name[name]routes/GET-:foo.jsroutes/GET-[foo].js
Optional dynamic segment:name?[[name]]routes/GET-:foo?.jsroutes/GET-[[foo]].js
Folder dynamic segment:name[name]routes/:foo/GET-bar.jsroutes/[foo]/GET-bar.js
Wildcard dynamic segment*[...]routes/foo/GET-*.jsroutes/foo/GET-[...].js

If you encounter any errors with the square bracket based syntax, please get in touch with the Gadget staff on Discord.

Was this page helpful?