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 filename | Matched requests |
---|---|
api/routes/GET.js | GET / |
api/routes/GET-foo.js | GET /foo or GET /foo/ , but not GET /bar or POST /foo |
api/routes/GET-[id].js | GET /foo , GET /bar , GET /1 but not POST /foo or GET /foo/bar |
api/routes/blogs/GET-[id].js | GET /blogs/1 , GET /blogs/welcome-to-gadget |
api/routes/blogs/POST.js | POST /blogs , but not POST /blogs/1 |
api/routes/category/[category]/blogs/GET-[id].js | GET /category/cooking/blogs/1 or GET /category/baking/blogs/5 |
api/routes/repos/GET-[...].js | GET /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.
1import type { RouteHandler } from "gadget-server";23const 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};1314export default route;
1import type { RouteHandler } from "gadget-server";23const 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};1314export 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.
1import type { RouteHandler } from "gadget-server";23const 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};1314export default route;
1import type { RouteHandler } from "gadget-server";23const 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};1314export 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
, likeGET-list.js
POST
, likePOST-create.js
PUT
, likePUT-update.js
PATCH
, likePATCH-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.
1import type { RouteHandler } from "gadget-server";23const 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};1213export default route;
1import type { RouteHandler } from "gadget-server";23const 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};1213export 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.
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply.send("hello world");5};67export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply.send("hello world");5};67export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply.send({ message: "hello world" });5};67export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply.send({ message: "hello world" });5};67export default route;
1// Plain text response with custom status code2import type { RouteHandler } from "gadget-server";34const route: RouteHandler = async ({ request, reply }) => {5 await reply6 .code(418)7 .type("text/plain; charset=utf-8")8 .send("I can't brew you any coffee because I'm a teapot!");9};1011export default route;
1// Plain text response with custom status code2import type { RouteHandler } from "gadget-server";34const route: RouteHandler = async ({ request, reply }) => {5 await reply6 .code(418)7 .type("text/plain; charset=utf-8")8 .send("I can't brew you any coffee because I'm a teapot!");9};1011export default route;
1// Accessing a captured id argument2import type { RouteHandler } from "gadget-server";34const 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};1415export default route;
1// Accessing a captured id argument2import type { RouteHandler } from "gadget-server";34const 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};1415export 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.
1// in routes/+views.ts2import FastifyView from "point-of-view";3import type { Server } from "gadget-server";45export 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.ts2import FastifyView from "point-of-view";3import type { Server } from "gadget-server";45export 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/+redirect.ts2import type { Server } from "gadget-server";34export 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.ts2import type { Server } from "gadget-server";34export 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/+custom-routes.ts2import type { Server } from "gadget-server";34export 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.ts2import type { Server } from "gadget-server";34export 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 folder3 admin/4 +auth.js // will only affect routes in the admin folder5 GET.js6 GET-posts.js7 users/8 +customizations.js // will only affect the one route below, and nothing in the routes/admin folder9 GET.js10 posts/11 +images.js // will only affect these posts routes in routes/posts12 GET.js13 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.
1// in api/boot/errors.ts2import type { Server } from "gadget-server";34export 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.ts2import type { Server } from "gadget-server";34export 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:
Name | Old syntax | New syntax | Old example | New Example |
---|---|---|---|---|
Single dynamic segment | :name | [name] | routes/GET-:foo.js | routes/GET-[foo].js |
Optional dynamic segment | :name? | [[name]] | routes/GET-:foo?.js | routes/GET-[[foo]].js |
Folder dynamic segment | :name | [name] | routes/:foo/GET-bar.js | routes/[foo]/GET-bar.js |
Wildcard dynamic segment | * | [...] | routes/foo/GET-*.js | routes/foo/GET-[...].js |
If you encounter any errors with the square bracket based syntax, please get in touch with the Gadget staff on Discord.