Route configuration 

HTTP routes accept a wide variety of options to change route behavior. Route options are set for a route by setting options on the route handler which must be the default export from your file:

Setting route options
JavaScript
1export default async function route({ request, reply }) {
2 await reply.send("hello " + request.query.name);
3}
4
5route.options = {
6 // add route options like \`schema\`, \`errorHandler\`, etc here. for example, we validate the query string has a name key
7 schema: {
8 querystring: {
9 type: "object",
10 properties: {
11 name: { type: "string" },
12 },
13 required: ["name"],
14 },
15 },
16};

Gadget routes are built on top of Fastify, which means your route's options can make use of all the same options as Fastify routes. For a full list of options, see the Fastify route options documentation.

Route options 

  • schema: an object containing the schemas for the request and response. They need to be in JSON Schema format, check fastify's docs for more info.

    • body: validates the body of the request if it is a POST, PUT, or PATCH method.
    • querystring or query: validates the querystring. This can be a complete JSON Schema object, with the property type of object and properties object of parameters, or simply the values of what would be contained in the properties object as shown below.
    • params: validates the params.
    • response: filter and generate a schema for the response, setting a schema allows us to have 10-20% more throughput.
  • exposeHeadRoute: creates a sibling HEAD route for any GET routes. Defaults to the value of exposeHeadRoutes instance option. If you want a custom HEAD handler without disabling this option, make sure to define it before the GET route.

  • attachValidation: attach validationError to request, if there is a schema validation error, instead of sending the error to the error handler. The default error format is the Ajv one.

  • onRequest(request, reply, done): a function called as soon as a request is received, it could also be an array of functions.

  • preParsing(request, reply, done): a function called before parsing the request, it could also be an array of functions.

  • preValidation(request, reply, done): a function called after the shared preValidation hooks, useful if you need to perform authentication at route level for example, it could also be an array of functions.

  • preHandler(request, reply, done): a function called just before the request handler, it could also be an array of functions.

  • preSerialization(request, reply, payload, done): a function called just before the serialization, it could also be an array of functions.

  • onSend(request, reply, payload, done): a function called right before a response is sent, it could also be an array of functions.

  • onResponse(request, reply, done): a function called when a response has been sent, so you will not be able to send more data to the client. It could also be an array of functions.

  • onTimeout(request, reply, done): a function called when a request is timed out and the HTTP socket has been hanged up.

  • onError(request, reply, error, done): a function called when an Error is thrown or send to the client by the route handler.

  • handler({ request, reply }): the function that will handle this request. The Fastify server will be bound to this when the handler is called. Note: using an arrow function will break the binding of this.

  • errorHandler(error, request, reply): a custom error handler for the scope of the request. Overrides the default error global handler, and anything set by setErrorHandler in plugins, for requests to the route. To access the default handler, you can access instance.errorHandler. Note that this will point to fastify's default errorHandler only if a plugin hasn't overridden it already.

  • validatorCompiler({ schema, method, url, httpPart }): function that builds schemas for request validations. See the Validation and Serialization documentation.

  • serializerCompiler({ { schema, method, url, httpStatus } }): function that builds schemas for response serialization. See the Validation and Serialization documentation.

  • schemaErrorFormatter(errors, dataVar): function that formats the errors from the validation compiler. See the Validation and Serialization documentation. Overrides the global schema error formatter handler, and anything set by setSchemaErrorFormatter, for requests to the route.

  • bodyLimit: prevents the default JSON body parser from parsing request bodies larger than this number of bytes. Must be an integer. You may also set this option globally when first creating the Fastify instance with fastify(options). Defaults to 1048576 (1 MiB).

  • logLevel: set log level for this route.

  • logSerializers: set serializers to log for this route.

  • config: object used to store custom configuration.

  • prefixTrailingSlash: string used to determine how to handle passing / as a route with a prefix.

    • both (default): Will register both /prefix and /prefix/.
    • slash: Will register only /prefix/.
    • no-slash: Will register only /prefix.

See Fastify's docs for exhaustive documentation on these route options.

Route context 

The request context parameter provides many of the same context properties that are available in actions.

Context keyDescription
requestthe Request object describing the incoming HTTP request
replythe Reply object for sending an HTTP response
apia connected, authorized instance of the generated API client for the current Gadget application. See the API Reference for more details on this object's interface.
applicationSessiona record representing the current user's session, if there is one.
applicationSessionIDthe ID of the record representing the current user's session, if there is one.
connectionsan object containing client objects for all connections. Read the connections guide to see what each connection provides.
loggera logger object suitable for emitting log entries viewable in Gadget's Log Viewer.
configan object of all the environment variables created in Gadget's Environment Variables editor.
currentAppUrlthe current url for the environment. e.g. https://my-app.gadget.app

For example, we can use the api object to make API calls to your application's API, and return them as JSON:

routes/GET-example.js
JavaScript
export default async function route({ request, reply, api }) {
const records = await api.someModel.findMany();
await reply.code(200).send({ result: records });
}

We can use the logger object to emit log statements about our request:

api/routes/GET-protected.js
JavaScript
1export default async function route({ request, reply, logger }) {
2 if (request.headers["authorization"] == "Bearer secret-token") {
3 logger.info({ ip: request.ip }, "access granted");
4 await reply.code(200).send({ result: "the protected stuff" });
5 } else {
6 logger.info({ ip: request.ip }, "access denied");
7 await reply.code(403).send({ error: "access denied" });
8 }
9}

The connections object passed to each route handler contains a client object for each connection. For example, we can make an API call for a particular Shopify shop:

api/routes/GET-protected.js
JavaScript
export default async function route({ request, reply, connections }) {
// relies on this request being made from an embedded app to know what \`shopify.current\` is
const result = await connections.shopify.current.shop.get();
await reply.code(200).send({ result });
}

Here's a full example demonstrating many of the elements of the request context:

api/routes/GET-my-route.js
JavaScript
1/**
2 * An example API route which finds a product for the current shopify shop, updates a model counter, and sends an SMS notification
3 **/
4export default async function route({
5 request,
6 reply,
7 api,
8 applicationSession,
9 connections,
10 logger,
11 config,
12}) {
13 // use "applicationSession" to the get current shopId
14 const shopId = applicationSession?.get("shop");
15
16 // if there's a current in context shopify client let's continue
17 if (connections.shopify.current) {
18 const product = await api.shopifyProduct.findById(request.body.productId);
19
20 // if the product belongs to the shop then update product with description from body
21 if (product.get("shop") == shopId) {
22 await connections.shopify.current.product.update(request.body.productId, {
23 body: request.body.productDescription,
24 });
25
26 // update count of updated products
27 const updatedProductRecord = await api.updatedProduct.findFirst({
28 filter: {
29 shop: {
30 equals: shopId,
31 },
32 },
33 });
34 // atomically increment the number of products we've updated in our updatedProduct model
35 await api.internal.updatedProduct.update(updatedProductRecord.id, {
36 _atomics: {
37 count: {
38 increment: 1,
39 },
40 },
41 });
42
43 // notify me via sms
44 const twilio = require("twilio");
45 // use "config" to pass along environment variables required by Twilio
46 const client = new twilio(config.accountSid, config.authToken);
47
48 await client.messages.create({
49 body: "Product has been updated!",
50 to: "+12345678901",
51 from: "+12345678901",
52 });
53
54 logger.info(
55 { productId: request.body.productId, shopId },
56 "a product has been updated"
57 );
58
59 await reply.send({ status: "ok" });
60 } else {
61 // couldn't find the product!
62 await reply.status(404).send();
63 }
64 } else {
65 // oops not authorized
66 await reply.status(401).send();
67 }
68}

The Request object 

The request object passed in the route context describes the incoming HTTP request, with properties for accessing the HTTP request headers, the request body, the matched route, and more. request is powered by Fastify, a high-performance HTTP framework for nodejs.

Request objects have the following fields:

FastifyRequest fieldDescription
querythe parsed query string from the incoming request, its format is specified by the route's querystringParser
bodythe request payload, see Content-Type Parser for details on what request payloads Fastify natively parses and how to support other content types
paramsthe params matching the URL
headersthe headers getter and setter
rawthe incoming HTTP request from Node core
idthe request ID
loga logger instance for the incoming request
ipthe IP address of the incoming request
hostnamethe host of the incoming request (derived from X-Forwarded-Host header when the trustProxy option is enabled). For HTTP/2 compatibility it returns :authority if no host header exists.
protocolthe protocol of the incoming request (will always be https on Gadget)
methodthe method of the incoming request
urlthe URL of the incoming request
routerMethodthe method defined for the router that is handling the request
routerPaththe path pattern defined for the router that is handling the request
is404true if the request is being handled by a 404 error handler, false if it is not
socketthe underlying connection of the incoming request
routeSchemathe scheme definition set for the router that is handling the request
routeConfigthe route config object
routeOptionsthe route option object passed when defining the route
bodyLimiteither the server-wide limit or route-specific limit on the size of the request body
methodthe HTTP method for the route, like GET, POST, or DELETE
urlthe path of the URL to match this route
logLevellog level defined for this route
versiona semver-compatible string that defines the version of the endpoint
exposeHeadRoutecreates a sibling HEAD route for any GET routes
.prefixTrailingSlashstring used to determine how to handle passing / as a route with a prefix.

For more details on the Request object, see the Request reference and the Fastify documentation.

The Reply object 

The reply object passed to each route in the context has functions for setting up and sending an HTTP response from your server for your route. The object is a FastifyReply object from Fastify, a high-performance HTTP framework for nodejs.

Reply objects have these functions and properties:

Reply propertyDescription
code(statusCode)sets the status code
status(statusCode)an alias for .code(statusCode)
statusCoderead and set the HTTP status code
header(name, value)sets a response header
headers(object)sets all the keys of the object as response headers
getHeader(name)retrieve the value of an already set header
getHeaders()gets a shallow copy of all current response headers
removeHeader(key)remove the value of a previously set header
hasHeader(name)determine if a header has been set
trailer(key, function)sets a response trailer
hasTrailer(key)determine if a trailer has been set
removeTrailer(key)remove the value of a previously set trailer
type(value)sets the header Content-Type
redirect([code,] dest)redirect to the specified URL with an optional status code. If not provided, the status code defaults to 302
callNotFound()invokes the custom not found handler
serialize(payload)serializes the specified payload using the default JSON serializer or using the custom serializer (if one is set) and returns the serialized payload
serializer(function)sets a custom serializer for the payload
send(payload)sends the payload to the user, could be a plain text, a buffer, JSON, stream, or an Error object
senta boolean value that you can use if you need to know if send has already been called
rawthe http.ServerResponse from Node core
logthe logger instance of the incoming request
requestthe incoming request
contextaccess the request's context property

For more details on the Reply object, see the Reply reference and the Fastify documentation.

Sending responses 

The .send() function on the FastifyReply object sends a response to the requesting client. .send() accepts a variety of datatypes and behaves differently depending on what is passed:

Sending objects 

Calling .send() with an object will send the object as a JSON response. If your route has an output schema defined using route.options.schema, that schema will be used for a performant JSON serialization of the object, and otherwise JSON.stringify() will be used.

Send an object as JSON
JavaScript
export default async function route({ request, reply }) {
await reply.send({ foo: "bar" });
}
Sending strings 

Calling .send() with a string behaves differently if you have set the value of the Content-Type header or not.

  • if Content-Type is not set, Gadget will send the string as a raw response right to the client, without any intermediate serialization or deserialization.
  • if Content-Type is set to application/json, Gadget will send the string as is, expecting it to contain valid JSON
  • if Content-Type is set, Gadget will attempt to serialize the string using the content type parser assigned to the set content type. If a custom content type serializer hasn't been set, the string will be sent unmodified
Send a raw string
JavaScript
export default async function route({ request, reply }) {
await reply.send("hello world");
}
Streaming replies 

Gadget supports sending response streams to the browser, instead of sending the response all at once. This allows long-running responses that send data as it is available, subscribe to upstream datastreams, or do anything else that produces data over time.

To send a streaming response, call reply.send with a nodejs ReadableStream object. Readable streams can be created by reading files, remote resources like proxied HTTP requests, etc. If you are sending a stream and you have not set a Content-Type header, send will set it to application/octet-stream.

You can stream any readable stream:

routes/GET-readable-stream.js
JavaScript
1import { Readable } from "stream";
2import { setTimeout } from "timers/promises";
3
4export default async function route({ request, reply }) {
5 // create a stream object
6 const stream = new Readable({
7 read() {},
8 encoding: "utf8",
9 });
10
11 // start sending the stream to the client
12 void reply.type("text/plain").send(stream);
13
14 // push the current time to the stream every second for 10 seconds
15 let counter = 0;
16 while (counter++ < 10) {
17 stream.push(`${new Date()}\n`);
18 await setTimeout(1000);
19 }
20
21 // end the stream
22 stream.push(null);
23}

If you have a stream of some other sort like an event emitter or a function that calls a callback, you must convert it to a Readable stream object before sending it to get a streaming response.

Streaming OpenAI chat completions 

Gadget has built in support for replying with a streaming response from OpenAI's node client (version 4 or higher) which is part of the connections object when the OpenAI connection is installed.

routes/POST-chat.js
JavaScript
1import { openAIResponseStream } from "gadget-server/ai";
2
3export default async function route({ request, reply, connections }) {
4 const stream = await connections.openai.chat.completions.create({
5 model: "gpt-3.5-turbo",
6 messages: [{ role: "user", content: "Hello!" }],
7 stream: true,
8 });
9
10 await reply.send(openAIResponseStream(stream));
11}
Sending buffers 

Calling .send() with a buffer will send the raw bytes in the buffer to the client. If you haven't already set a Content-Type header, Gadget will set it to application/octet-stream before sending the response.

Send a buffer
JavaScript
export default async function route({ request, reply }) {
const buffer = Buffer.from("hello world");
await reply.type("text/plain").send(buffer);
}

Response Caching 

Gadget supports caching your HTTP route responses within a high-performance CDN. This is useful for giving the best experience to your users by serving requests at the edge close to them, and for avoiding re-generating expensive responses too often.

Caching with Gadget is driven entirely by the standard Cache-Control HTTP response header, which you can set with the reply.header or reply.headers function.

For example, you can set the content of an HTTP route to be cached for 1 hour with the following:

JavaScript
export default async function ({ request, reply }) {
await reply
.header("Cache-Control", "public, max-age=3600")
.send("this response will be cached for 1 hour");
}

For more information on valid Cache-Control header values, see the MDN docs.

Caching for a duration of time 

Set the Cache-Control header to public, max-age=<seconds> to have Gadget's CDN cache the content for the specified number of seconds. For example, to cache a response for 1 hour, you can set the header to public, max-age=3600, or to cache for one day, you can set the header to public, max-age=86400.

JavaScript
export default async function ({ request, reply }) {
await reply
.header("Cache-Control", "public, max-age=3600")
.send("this will be cached for 1 hour");
}

Caching forever 

Set the Cache-Control header to public, immutable, max-age=31536000 to have Gadget's CDN cache the content for as long as possible. This is appropriate only for content that you know won't change, or where it is ok for some clients to have permanently stale data.

JavaScript
export default async function ({ request, reply }) {
await reply
.header("Cache-Control", "public, immutable, max-age=31536000")
.send("this will be cached for as long as possible in the CDN and in browsers");
}

Preventing caching 

Caching can be explicitly disabled by setting the Cache-Control header to private, no-store. This is appropriate for content that is sensitive or that you don't want cached for any reason. Generally, browsers won't cache data if there is no Cache-Control header present at all, but you can add this header to make it explicit.

JavaScript
export default async function ({ request, reply }) {
await reply
.header("Cache-Control", "private, no-store")
.send("this will be never be cached by the CDN or browser");
}

Asking browsers to revalidate 

The Cache-Control header can contain instructions to browsers allowing them to serve stale content while re-fetching up-to-date data in the background.

Set the Cache-Control header to max-age=1, stale-while-revalidate=59 to have Gadget's CDN cache the content for one minute while instructing the browser and CDN to refetch the content if it is older than one second. This is appropriate for data that is changing somewhat often, but where you would rather users see somewhat stale data and save request processing time.

JavaScript
1export default async function ({ request, reply }) {
2 await reply
3 .header("Cache-Control", "max-age=1, stale-while-revalidate=59")
4 .send(
5 "this will be served to users up to one minute out of date, and refetched in the background."
6 );
7}

For more information on the stale-while-revalidate directive, see this introduction or the MDN docs.

Default HTTP route caching behavior 

By default, HTTP route contents are never cached unless you set the Cache-Control or CDN-Cache-Control header. If you set either of those headers, caching is enabled.

API response caching 

Your app's GraphQL API is not cached at an HTTP layer so clients are never served out-of-date data. Gadget implements caching within your GraphQL backend to make your responses fast while remaining fresh.

Currently, you can't have your API responses set custom Cache-Control headers. If you want to create a CDN-cached response or browser-cached response for API data, you must use an HTTP route to fetch data from the API, and serve it with headers of your choosing.

For example, we can request a list of the most recent records of the post model from an app's GraphQL API in an HTTP route, and serve a cached response there:

routes/GET-recent-posts.js
JavaScript
1export default async function ({ request, reply, api }) {
2 const posts = await api.post.findMany({
3 sort: { createdAt: "Descending" },
4 first: 10,
5 });
6 await reply.header("cache-control", "public, max-age=86400").send({ posts });
7}

Cache expiry 

If you set a Cache-Control or CDN-Cache-Control header, Gadget's CDN and/or users' browsers will cache your content for the duration you specify in the header. There isn't a way to explicitly expire this cache in users browsers or within Gadget's CDN. Please get in touch with the Gadget team on Discord if you'd like to explore custom cache expiry solutions.

CORS configuration 

Gadget applications often serve requests made from domains other than your-app.gadget.app which makes them cross-domain and governed by browser Cross Origin Resource Sharing (CORS) rules. If you're making requests to Gadget from a Next.js app hosted on another domain, a Shopify storefront, or anywhere other than your-app.gadget.app, you need to make sure you have configured your app to respond to CORS requests correctly.

To set up CORS headers for your app, we suggest using @fastify/cors. @fastify/cors is a high-quality plugin for handling CORS requests from the Fastify ecosystem available on npm.

To start, open your Gadget command palette ( P or Ctrl P) and enter yarn add @fastify/cors. It will be added to your project's package.json file.

You can also manually add @fastify/cors to your package.json and click Run yarn to install it:

package.json
json
1{
2 "dependencies": {
3 "@fastify/cors": "^8.5.0"
4 // ...
5 }
6}

After, you can mount @fastify/cors in your application with a Route plugin file at routes/+scope.js:

routes/+scope.js route plugin file
JavaScript
1import cors from "@fastify/cors";
2export default async function (server) {
3 await server.register(cors, {
4 // allow CORS requests from my-cool-frontend.com
5 origin: ["https://my-cool-frontend.com"],
6 // allow GET, POST, and PUT requests
7 methods: ["GET", "POST", "PUT"],
8 // other options, see here: https://www.npmjs.com/package/fastify-cors
9 });
10}

When your application boots, this route plugin will be required, and configure @fastify/cors to send the right Access-Control-Allow-Origin header to browsers.

Just like all route plugins, +scope.js files that mount @fastify/cors will only affect requests to route contained in the same folder/subfolder as the +scope.js file. Registering @fastify/cors directly in routes/+scope.js will affect all the routes of your app, or registering it in routes/api/+scope.js will only affect requests to any route in that routes/api folder. This allows different CORS configurations for different parts of your application.

On Gadget framework v0.1 or v0.2?

If your Gadget app is on framework version v0.1 or v0.2, you'll need to install fastify-cors instead of @fastify/cors. You can do this by manually adding it to your package.json file and clicking Run yarn:

package.json
json
1{
2 "dependencies": {
3 "fastify-cors": "6.0.3"
4 // ...
5 }
6}

Allowing requests from any domain 

If your app is intended for any domain, you can configure @fastify/cors to allow any origin to make requests to your app with origin: true.

routes/+scope.js route plugin file
JavaScript
1import cors from "@fastify/cors";
2export default async function (server) {
3 await server.register(cors, {
4 origin: true, // allow requests from any domain
5 });
6}

Fine-grained CORS configuration 

If you want to avoid setting CORS headers for your entire application, you can also manually manage CORS headers using reply.header in a route file, or add OPTIONS-*.js routes for handling CORS preflight requests yourself.

For example, we can reply with a base set of CORS headers right in a route file:

routes/POST-foo.js route file
JavaScript
1export default async function route({ request, reply }) {
2 await reply
3 // set reply headers for CORS explicitly
4 .headers({
5 "access-control-allow-origin": "*",
6 "access-control-allow-methods": "POST, GET, OPTIONS",
7 "access-control-allow-headers": "Content-Type",
8 })
9 // send the response
10 .send("foo");
11}

Or for cross-origin requests which require preflight requests, we can handle these requests explicitly by adding an OPTIONS-foo.js route handler:

routes/OPTIONS-foo.js route file
JavaScript
1// OPTIONS route handler for an adjacent routes/PUT-foo.js route file
2export default async function route({ request, reply }) {
3 await reply
4 // set reply headers for CORS preflight request
5 .headers({
6 "access-control-allow-origin": "*",
7 "access-control-allow-methods": "PUT, GET, OPTIONS",
8 "access-control-allow-headers": "Content-Type",
9 })
10 // send no response to meet the preflight request spec
11 .send("")
12 .code(200);
13}

Note that if using @fastify/cors, you don't need to add manual CORS header setting or explicit OPTIONS-*.js routes as @fastify/cors handles this for you.

Multipart requests 

Gadget applications don't support requests that use Content-Type: multipart/form-data (multipart requests) by default, but you can add multipart support using the @fastify/multipart package from npm.

If you're processing file uploads, you can utilize Gadget's built-in support for file storage and uploading using the file field type. See the Storing Files guide for more information.

First, you need to install @fastify/multipart from npm. Open your Gadget command palette ( P or Ctrl P) and enter yarn add @fastify/multipart. It will be added to your project's package.json file.

You can also manually add @fastify/multipart to your package.json and click Run yarn to install it:

package.json
json
{
"dependencies": {
"@fastify/multipart": "^8.0.0"
}
}

Next, you need to mount @fastify/multipart into your server with a boot or route plugin. A boot plugin will register it globally, and a route plugin will register it for only some routes. To keep things simple let's demonstrate with a boot plugin:

boot/multipart.js
JavaScript
import FastifyMultipart from "@fastify/multipart";
export default async function (server) {
await server.register(FastifyMultipart);
}

With the plugin registered, you can access POSTed file data using request.file or request.files in your routes:

routes/POST-upload.js
JavaScript
1export default async function route({ request, reply }) {
2 // process a single file
3 const data = await request.file();
4
5 data.file; // the stream of file data, is a node ReadableStream object
6 data.fields; // other parsed parts of the multipart request
7 data.fieldname; // the name of the key the file is stored under in the incoming data
8 data.filename; // the name of the file if provided
9 data.encoding; // the file encoding
10 data.mimetype; // the mimetype inferred from the file using the extension
11
12 // do something with the file
13
14 reply.send({ status: "ok" });
15}

For more information on working with @fastify/multipart, see the @fastify/multipart docs.

On Gadget framework v0.1 or v0.2?

If your Gadget app is on framework version v0.1 or v0.2, you'll need to install fastify-multipart instead of @fastify/multipart. You can do this by manually adding it to your package.json file and clicking Run yarn:

package.json
json
1{
2 "dependencies": {
3 "fastify-multipart": "5.3.1"
4 // ...
5 }
6}