# 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: ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler<{ Querystring: { name: string } }> = async ({ request, reply, }) => { await reply.send("hello " + request.query.name); }; route.options = { // add route options like `schema`, `errorHandler`, etc here. for example, we validate the query string has a name key schema: { querystring: { type: "object", properties: { name: { type: "string" }, }, required: ["name"], }, }, }; export default route; ``` Gadget routes are built on top of [Fastify](https://www.fastify.dev/), 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](https://www.fastify.dev/docs/latest/Reference/Routes/#route-options). ## Route options  * `schema`: an object containing the schemas for the request and response. They need to be in [JSON Schema](https://json-schema.org/) format, check [fastify's docs](https://www.fastify.dev/docs/latest/Reference/Validation-and-Serialization/) 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`](https://www.fastify.dev/docs/latest/Reference/Server#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](https://ajv.js.org/api.html#error-objects) is the Ajv one. * `onRequest(request, reply, done)`: a [function](https://www.fastify.dev/docs/latest/Reference/Hooks#onrequest) called as soon as a request is received, it could also be an array of functions. * `preParsing(request, reply, done)`: a [function](https://www.fastify.dev/docs/latest/Reference/Hooks#preparsing) called before parsing the request, it could also be an array of functions. * `preValidation(request, reply, done)`: a [function](https://www.fastify.dev/docs/latest/Reference/Hooks#prevalidation) 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](https://www.fastify.dev/docs/latest/Reference/Hooks#prehandler) called just before the request handler, it could also be an array of functions. * `preSerialization(request, reply, payload, done)`: a [function](https://www.fastify.dev/docs/latest/Reference/Hooks#preserialization) called just before the serialization, it could also be an array of functions. * `onSend(request, reply, payload, done)`: a [function](https://www.fastify.dev/docs/latest/Reference/Hooks#route-hooks) called right before a response is sent, it could also be an array of functions. * `onResponse(request, reply, done)`: a [function](https://www.fastify.dev/docs/latest/Reference/Hooks#onresponse) 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](https://www.fastify.dev/docs/latest/Reference/Hooks#ontimeout) called when a request is timed out and the HTTP socket has been hanged up. * `onError(request, reply, error, done)`: a [function](https://www.fastify.dev/docs/latest/Reference/Hooks#onerror) 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](https://www.fastify.dev/docs/latest/Reference/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](https://www.fastify.dev/docs/latest/Reference/Validation-and-Serialization/#schema-validator) documentation. * `serializerCompiler({ { schema, method, url, httpStatus } })`: function that builds schemas for response serialization. See the [Validation and Serialization](https://www.fastify.dev/docs/latest/Reference/Validation-and-Serialization/#schema-serializer) documentation. * `schemaErrorFormatter(errors, dataVar)`: function that formats the errors from the validation compiler. See the [Validation and Serialization](https://www.fastify.dev/docs/latest/Reference/Validation-and-Serialization/#error-handling) documentation. Overrides the global schema error formatter handler, and anything set by `setSchemaErrorFormatter`, for requests to the route. * `cors`: set Cross Origin Resource Sharing (CORS) options for this route. See the section for more details. * `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`. `schema`: an object containing the schemas for the request and response. They need to be in [JSON Schema](https://json-schema.org/) format, check [fastify's docs](https://www.fastify.dev/docs/latest/Reference/Validation-and-Serialization/) 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`](https://www.fastify.dev/docs/latest/Reference/Server#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](https://ajv.js.org/api.html#error-objects) is the Ajv one. `onRequest(request, reply, done)`: a [function](https://www.fastify.dev/docs/latest/Reference/Hooks#onrequest) called as soon as a request is received, it could also be an array of functions. `preParsing(request, reply, done)`: a [function](https://www.fastify.dev/docs/latest/Reference/Hooks#preparsing) called before parsing the request, it could also be an array of functions. `preValidation(request, reply, done)`: a [function](https://www.fastify.dev/docs/latest/Reference/Hooks#prevalidation) 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](https://www.fastify.dev/docs/latest/Reference/Hooks#prehandler) called just before the request handler, it could also be an array of functions. `preSerialization(request, reply, payload, done)`: a [function](https://www.fastify.dev/docs/latest/Reference/Hooks#preserialization) called just before the serialization, it could also be an array of functions. `onSend(request, reply, payload, done)`: a [function](https://www.fastify.dev/docs/latest/Reference/Hooks#route-hooks) called right before a response is sent, it could also be an array of functions. `onResponse(request, reply, done)`: a [function](https://www.fastify.dev/docs/latest/Reference/Hooks#onresponse) 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](https://www.fastify.dev/docs/latest/Reference/Hooks#ontimeout) called when a request is timed out and the HTTP socket has been hanged up. `onError(request, reply, error, done)`: a [function](https://www.fastify.dev/docs/latest/Reference/Hooks#onerror) 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](https://www.fastify.dev/docs/latest/Reference/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](https://www.fastify.dev/docs/latest/Reference/Validation-and-Serialization/#schema-validator) documentation. `serializerCompiler({ { schema, method, url, httpStatus } })`: function that builds schemas for response serialization. See the [Validation and Serialization](https://www.fastify.dev/docs/latest/Reference/Validation-and-Serialization/#schema-serializer) documentation. `schemaErrorFormatter(errors, dataVar)`: function that formats the errors from the validation compiler. See the [Validation and Serialization](https://www.fastify.dev/docs/latest/Reference/Validation-and-Serialization/#error-handling) documentation. Overrides the global schema error formatter handler, and anything set by `setSchemaErrorFormatter`, for requests to the route. `cors`: set Cross Origin Resource Sharing (CORS) options for this route. See the section for more details. `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](https://www.fastify.dev/docs/latest/Reference/Routes/) 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 key | Description | | --- | --- | | `request` | the [`Request`](https://docs.gadget-canary.xyz/reference/gadget-server#request) object describing the incoming HTTP request | | `reply` | the [`Reply`](https://docs.gadget-canary.xyz/reference/gadget-server#reply) object for sending an HTTP response | | `api` | a connected, authorized instance of the generated API client for the current Gadget application. See the [API Reference](https://docs.gadget-canary.xyz/api/example-app) for more details on this object's interface. | | `applicationSession` | a record representing the current user's session, if there is one. | | `applicationSessionID` | the ID of the record representing the current user's session, if there is one. | | `connections` | an object containing client objects for all connections. Read the [connections guide](https://docs.gadget-canary.xyz/guides/plugins) to see what each connection provides. | | `logger` | a [logger](https://docs.gadget-canary.xyz/guides/development-tools/logger) object suitable for emitting log entries viewable in Gadget's Log Viewer. | | `config` | an object of all the environment variables created in Gadget's Environment Variables editor. | | `currentAppUrl` | the 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: ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply, api }) => { const records = await api.someModel.findMany(); await reply.code(200).send({ result: records }); }; export default route; ``` We can use the [`logger`](https://docs.gadget-canary.xyz/guides/development-tools/logger#logger-api) object to emit log statements about our request: ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply, logger }) => { if (request.headers["authorization"] == "Bearer secret-token") { logger.info({ ip: request.ip }, "access granted"); await reply.code(200).send({ result: "the protected stuff" }); } else { logger.info({ ip: request.ip }, "access denied"); await reply.code(403).send({ error: "access denied" }); } }; export default route; ``` 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: ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ 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 }); }; export default route; ``` Here's a full example demonstrating many of the elements of the request context: ```typescript import type { RouteHandler } from "gadget-server"; /** * An example API route which finds a product for the current shopify shop, updates a model counter, and sends an SMS notification **/ const route: RouteHandler<{ Body: { productId: string; productDescription: string; }; }> = async ({ request, reply, api, applicationSession, connections, logger, config, }) => { // use "applicationSession" to the get current shopId const shopId = applicationSession?.get("shop"); // if there's a current in context shopify client let's continue if (connections.shopify.current) { const product = await api.shopifyProduct.findById(request.body.productId); // if the product belongs to the shop then update product with description from body if (product.get("shop") == shopId) { await connections.shopify.current.product.update(request.body.productId, { body: request.body.productDescription, }); // update count of updated products const updatedProductRecord = await api.updatedProduct.findFirst({ filter: { shopId: { equals: shopId, }, }, }); // atomically increment the number of products we've updated in our updatedProduct model await api.internal.updatedProduct.update(updatedProductRecord.id, { _atomics: { count: { increment: 1, }, }, }); // notify me via sms const twilio = require("twilio"); // use "config" to pass along environment variables required by Twilio const client = new twilio(config.accountSid, config.authToken); await client.messages.create({ body: "Product has been updated!", to: "+12345678901", from: "+12345678901", }); logger.info( { productId: request.body.productId, shopId }, "a product has been updated" ); await reply.send({ status: "ok" }); } else { // couldn't find the product! await reply.status(404).send(); } } else { // oops not authorized await reply.status(401).send(); } }; export default route; ``` ### 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` field | Description | | --- | --- | | `query` | the parsed query string from the incoming request, its format is specified by the route's `querystringParser` | | `body` | the request payload, see [Content-Type Parser](https://www.fastify.dev/docs/latest/Reference/ContentTypeParser/) for details on what request payloads Fastify natively parses and how to support other content types | | `params` | the params matching the URL | | `headers` | the headers getter and setter | | `raw` | the incoming HTTP request from Node core | | `id` | the request ID | | `log` | a logger instance for the incoming request | | `ip` | the IP address of the incoming request | | `hostname` | the 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. | | `protocol` | the protocol of the incoming request (will always be `https` on Gadget) | | `method` | the method of the incoming request | | `url` | the URL of the incoming request | | `routerMethod` | the method defined for the router that is handling the request | | `routerPath` | the path pattern defined for the router that is handling the request | | `is404` | true if the request is being handled by a 404 error handler, false if it is not | | `socket` | the underlying connection of the incoming request | | `routeSchema` | the scheme definition set for the router that is handling the request | | `routeConfig` | the route config object | | `routeOptions` | the route option object passed when defining the route | | `bodyLimit` | either the server-wide limit or route-specific limit on the size of the request body | | `method` | the HTTP method for the route, like `GET`, `POST`, or `DELETE` | | `url` | the path of the URL to match this route | | `logLevel` | log level defined for this route | | `version` | a semver-compatible string that defines the version of the endpoint | | `exposeHeadRoute` | creates a sibling HEAD route for any GET routes | | `.prefixTrailingSlash` | string used to determine how to handle passing / as a route with a prefix. | For more details on the `Request` object, see the [`Request` reference](https://docs.gadget-canary.xyz/reference/gadget-server#request) and the [Fastify documentation](https://www.fastify.dev/docs/v3.29.x/Reference/Request/). ### 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](https://www.fastify.dev/docs/v3.29.x/Reference/Reply/), a high-performance HTTP framework for nodejs. `Reply` objects have these functions and properties: | `Reply` property | Description | | --- | --- | | `code(statusCode)` | sets the status code | | `status(statusCode)` | an alias for `.code(statusCode)` | | `statusCode` | read 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 | | `sent` | a boolean value that you can use if you need to know if `send` has already been called | | `raw` | the [`http.ServerResponse`](https://nodejs.org/dist/latest-v14.x/docs/api/http.html#http_class_http_serverresponse) from Node core | | `log` | the logger instance of the incoming request | | `request` | the incoming request | | `context` | access the request's context property | For more details on the `Reply` object, see the [`Reply` reference](https://docs.gadget-canary.xyz/reference/gadget-server#reply) and the [Fastify documentation](https://www.fastify.dev/docs/v3.29.x/Reference/Reply/). #### 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. ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply }) => { await reply.send({ foo: "bar" }); }; export default route; ``` ##### 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 ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply }) => { await reply.send("hello world"); }; export default route; ``` ##### 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`](https://nodejs.org/api/stream.html#readable-streams) 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: ```typescript import { Readable } from "stream"; import { setTimeout } from "timers/promises"; import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply }) => { // create a stream object const stream = new Readable({ read() {}, encoding: "utf8", }); // start sending the stream to the client void reply.type("text/plain").send(stream); // push the current time to the stream every second for 10 seconds let counter = 0; while (counter++ < 10) { stream.push(`${new Date()}\n`); await setTimeout(1000); } // end the stream stream.push(null); }; export default route; ``` 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](https://docs.gadget-canary.xyz/guides/plugins/openai) is installed. ```typescript import { openAIResponseStream } from "gadget-server/ai"; import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply, connections }) => { const stream = await connections.openai.chat.completions.create({ model: "gpt-3.5-turbo", messages: [{ role: "user", content: "Hello!" }], stream: true, }); await reply.send(openAIResponseStream(stream)); }; export default route; ``` ##### 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. ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply }) => { const buffer = Buffer.from("hello world"); await reply.type("text/plain").send(buffer); }; export default route; ``` ## 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: ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply }) => { await reply .header("Cache-Control", "public, max-age=3600") .send("this response will be cached for 1 hour"); }; export default route; ``` For more information on valid `Cache-Control` header values, see the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control). ### Caching for a duration of time  Set the `Cache-Control` header to `public, max-age=` 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`. ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply }) => { await reply .header("Cache-Control", "public, max-age=3600") .send("this will be cached for 1 hour"); }; export default route; ``` ### 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. ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ 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"); }; export default route; ``` ### 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. ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply }) => { await reply .header("Cache-Control", "private, no-store") .send("this will never be cached by the CDN or browser"); }; export default route; ``` ### 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. ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply }) => { await reply .header("Cache-Control", "max-age=1, stale-while-revalidate=59") .send( "this will be served to users up to one minute out of date, and re-fetched in the background." ); }; export default route; ``` For more information on the `stale-while-revalidate` directive, see this [introduction](https://web.dev/stale-while-revalidate/) or the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives). ### 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: ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply, api }) => { const posts = await api.post.findMany({ sort: { createdAt: "Descending" }, first: 10, }); await reply.header("cache-control", "public, max-age=86400").send({ posts }); }; export default route; ``` ### 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)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) rules. If you are making requests to Gadget from a website hosted on another domain, like a Shopify storefront or a Next.js app, you need to configure your app to respond to CORS requests correctly. Gadget Routes have a built-in CORS header configuration system. You can set a CORS configuration for individual routes using the `route.options.cors` property, or you can set a CORS configuration for whole folders of routes in a route plugin with `server.setScopeCORS()`. ### Setting CORS headers for a route  To set CORS headers for a route, you can set the `cors` route option: ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply }) => { await reply.send("hello world"); }; route.options = { cors: { // allow only requests from https://my-cool-frontend.com origin: ["https://my-cool-frontend.com"], // allow only GET, POST, and PUT requests methods: ["GET", "POST", "PUT"], }, }; export default route; ``` The `route.options.cors` property expects a [`CORSRouteOptions` object](https://docs.gadget-canary.xyz/reference/gadget-server#corsrouteoptions) to configure the various CORS headers that can be set. ### Disallowing CORS access to a route  To disallow CORS access to a route, you can set the `cors` route option to `false`: ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply }) => { await reply.send("hello world"); }; route.options = { // will ensure no CORS headers are sent for this route // so browsers will prevent cross-origin requests to this route cors: false, }; export default route; ``` ### Setting CORS headers for a folder of routes  To set CORS headers for all the routes in a folder, use the `server.setScopeCORS()` function in a `+scope.ts` route plugin file to set CORS options for all routes in that scope: ```typescript import type { Server } from "gadget-server"; export default async function (server: Server) { server.setScopeCORS({ // allow only requests from https://my-cool-frontend.com origin: ["https://my-cool-frontend.com"], // allow only GET, POST, and PUT requests methods: ["GET", "POST", "PUT"], }); } ``` `server.setScopeCORS` takes the same [`CORSRouteOptions` options](https://docs.gadget-canary.xyz/reference/gadget-server#corsrouteoptions) object as the `cors` route option, and will apply them to all routes in the scope. Like all Gadget [route plugin files](https://docs.gadget-canary.xyz/guides/http-routes/route-structure#route-plugins), the location of the `+scope.js` file determines which routes are affected by the CORS configuration. All route files within the same folder as the `+scope.js` file will have the CORS options set by the `+scope.js` file applied to them. If you want to set CORS options for a route in a subfolder, you can create a `+scope.js` file in that subfolder. You can also set CORS options for all routes in your app by calling `server.setScopeCORS()` in a root-level route plugin file: ```typescript import type { Server } from "gadget-server"; export default async function (server: Server) { // set CORS options for all routes in the app server.setScopeCORS({ // watch out! this will all cross-origin requests to all routes in your app! origin: true, }); } ``` ### Allowing requests from any domain  If you'd like your route to be accessible from any domain, like say for use on any Shopify storefront, you can allow any origin to make requests to your app with `origin: true`. ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply }) => { await reply.send("hello world"); }; route.options = { cors: { // allow requests from any origin origin: true, // ... more options here if needed }, }; export default route; ``` ### Allowing certain headers  The [`Access-Control-Allow-Headers` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Headers) can be set to allow browsers to send along specific request headers values across origin boundaries. Use the `allowedHeaders` route option to pass which headers are allowed to be sent to your route. ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply }) => { await reply.send("hello world"); }; route.options = { cors: { // allow only the Authorization and Content-Type headers to be sent cross-origin to this route allowedHeaders: ["Authorization", "Content-Type"], }, }; export default route; ``` You can also use the `exposedHeaders` route option to pass which response headers are allowed to be exposed to the client, which sets the [`Access-Control-Expose-Headers` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers). ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply }) => { await reply.send("hello world"); }; route.options = { cors: { // allow only the Content-Range and X-Content-Range headers to be exposed to the client exposedHeaders: ["Content-Range", "X-Content-Range"], }, }; export default route; ``` ### Supported CORS options  The `cors` route option supports the following options: * `origin`: the origin or origins to allow requests from. Can be a string, an array of strings, a boolean, or a function that returns a string or array of strings. Defaults to `"*"`, which allows requests from any origin. * `methods`: the HTTP methods to allow requests for. Can be a string, an array of strings, or a function that returns a string or array of strings. Defaults to `["GET", "HEAD", "POST"]`. * `allowedHeaders`: the headers to allow requests for. Can be a string, an array of strings, or a function that returns a string or array of strings. Defaults to `undefined`, which will send all headers allowed by the request's `Access-Control-Request-Headers` header. * `exposedHeaders`: the headers to expose to the client. Can be a string, an array of strings, or a function that returns a string or array of strings. Defaults to `undefined`, which won't send any exposed headers for preflight requests. * `credentials`: whether to allow requests with credentials. Can be a boolean, or a function that returns a boolean. Defaults to `false`. * `maxAge`: the maximum age of the CORS preflight request in seconds. Can be a number, or a function that returns a number. Defaults to `undefined`, which won't send any max age headers for preflight requests. * `cacheControl`: the cache control directive to use for the CORS preflight request. Can be a number, or a function that returns a number. Defaults to `undefined`, which won't send any cache control headers for preflight requests. * `optionsSuccessStatus`: the status code to use for successful OPTIONS requests. Can be a number, or a function that returns a number. Defaults to `204`, which is the default status code for successful OPTIONS requests. * `strictPreflight`: whether to enforce strict requirement of the CORS preflight request headers (Access-Control-Request-Method and Origin). Can be a boolean, or a function that returns a boolean. Defaults to `true`. ### Controlling CORS for development assets  In development, your Gadget app's assets are served by Vite, which has an independent CORS configuration from your HTTP backend routes. You can control your development assets CORS configuration by setting the `server.cors` option in your Vite configuration. ```typescript import { defineConfig } from "vite"; export default defineConfig({ server: { cors: { // allow requests from any origin origin: true, }, }, }); ``` See the [Vite documentation](https://vitejs.dev/config/server-options/#server-cors) for more information on the `server.cors` option. ### Controlling CORS for production assets  In production, your Gadget app's assets are served by the Gadget CDN, and have fixed, permissive CORS headers. All assets uploaded to the Gadget CDN allow requests from any origin. This can't be changed. If you need control over your CORS headers, use an HTTP route to serve your content. ### Using `@fastify/cors`  If Gadget's built-in CORS support is insufficient for your use case, you can also use the [`@fastify/cors`](https://www.npmjs.com/package/fastify-cors) plugin. `@fastify/cors` is a high-quality plugin for handling CORS requests from the Fastify ecosystem available on npm that supports more advanced CORS options. Gadget's built-in CORS support is a great way to get started with CORS and is maintained by the Gadget team. Prefer using the built in `cors` option when possible. Open your Gadget command palette ( **P** or **Ctrl P**) and run the following to install the package: ```sh yarn add @fastify/cors@9.0.1 ``` You can also manually add `@fastify/cors` to your `package.json` and click **Run yarn** to install it: ```json // in package.json { "dependencies": { "@fastify/cors": "^9.0.1" // ... } } ``` Gadget framework version 1.X.X uses Fastify v4. You cannot install the latest version of `@fastify/cors` (v10.0.0+) because Fastify v5 is required. After, you can mount `@fastify/cors` in your application with a Route plugin file at `routes/+scope.js`: ```typescript import cors from "@fastify/cors"; import type { Server } from "gadget-server"; export default async function (server: Server) { await server.register(cors, { // allow CORS requests from my-cool-frontend.com origin: ["https://my-cool-frontend.com"], // allow GET, POST, and PUT requests methods: ["GET", "POST", "PUT"], // other options, see here: https://www.npmjs.com/package/fastify-cors }); } ``` 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. ## 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`](https://www.npmjs.com/package/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](https://docs.gadget-canary.xyz/guides/models/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: ```json // in package.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: ```typescript import FastifyMultipart from "@fastify/multipart"; import type { Server } from "gadget-server"; export default async function (server: Server) { await server.register(FastifyMultipart); } ``` With the plugin registered, you can access POSTed file data using `request.file` or `request.files` in your routes: ```typescript import type { RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ request, reply }) => { // process a single file const data = await request.file(); // the stream of file data, is a node ReadableStream object data.file; // other parsed parts of the multipart request data.fields; // the name of the key the file is stored under in the incoming data data.fieldname; // the name of the file if provided data.filename; // the file encoding data.encoding; // the mimetype inferred from the file using the extension data.mimetype; // do something with the file reply.send({ status: "ok" }); }; export default route; ``` For more information on working with `@fastify/multipart`, see the [@fastify/multipart docs](https://www.npmjs.com/package/fastify-multipart). ## Rate limiting  Rate limiting controls how many requests a client can make to your routes within a given time window. It protects your app from abuse, prevents unexpected cost spikes, and helps ensure fair usage across all clients. Gadget does not include built-in route-level rate limiting. You can add it using the [`@fastify/rate-limit`](https://www.npmjs.com/package/@fastify/rate-limit) plugin from npm. Gadget enforces platform-level rate limits on all apps. Route-level rate limiting with `@fastify/rate-limit` lets you set tighter, more granular limits for specific routes or groups of routes. ### Installing `@fastify/rate-limit`  Open your Gadget command palette ( **P** or **Ctrl P**) and run the following to install the package: ```sh yarn add @fastify/rate-limit@9 ``` You can also manually add `@fastify/rate-limit` to your `package.json` and click **Run yarn** to install it: ```json // in package.json { "dependencies": { "@fastify/rate-limit": "^9.0.0" } } ``` Gadget framework version 1.X.X uses Fastify v4. You cannot install the latest version of `@fastify/rate-limit` (v10.0.0+) because Fastify v5 is required. ### Applying rate limits  Register `@fastify/rate-limit` in a boot plugin to apply a rate limit to all routes in your app: ```typescript import rateLimit from "@fastify/rate-limit"; import type { Server } from "gadget-server"; export default async function (server: Server) { await server.register(rateLimit, { max: 100, timeWindow: "1 minute", }); } ``` To apply rate limiting to only a subset of routes, register the plugin in a `+scope.ts` route plugin file instead of a boot plugin. Like all route plugins, the location of the `+scope.ts` file determines which routes are affected. See [route plugins](https://docs.gadget-canary.xyz/guides/http-routes/route-structure#route-plugins) for more details. ```typescript import rateLimit from "@fastify/rate-limit"; import type { Server } from "gadget-server"; export default async function (server: Server) { await server.register(rateLimit, { max: 50, timeWindow: "1 minute", }); } ``` For a full list of options, see the [`@fastify/rate-limit` documentation](https://github.com/fastify/fastify-rate-limit). ## Fastify Server Configuration  Gadget configures some Fastify server options differently than their defaults: * `maxParamLength`: Set to 500 (Fastify default is 100) - This allows for longer parameter names in route URLs * `ignoreTrailingSlash`: Set to true (Fastify default is false) - This allows for routes to be matched without requiring a trailing slash * `bodyLimit`: Set to 100mb (Fastify default is 10mb) - This allows for larger request bodies to be processed These defaults can't be adjusted, but please contact Gadget support if they are incorrect for your use case.