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:
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler<{ Querystring: { name: string } }> = async ({4 request,5 reply,6}) => {7 await reply.send("hello " + request.query.name);8};910route.options = {11 // add route options like `schema`, `errorHandler`, etc here. for example, we validate the query string has a name key12 schema: {13 querystring: {14 type: "object",15 properties: {16 name: { type: "string" },17 },18 required: ["name"],19 },20 },21};2223export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler<{ Querystring: { name: string } }> = async ({4 request,5 reply,6}) => {7 await reply.send("hello " + request.query.name);8};910route.options = {11 // add route options like `schema`, `errorHandler`, etc here. for example, we validate the query string has a name key12 schema: {13 querystring: {14 type: "object",15 properties: {16 name: { type: "string" },17 },18 required: ["name"],19 },20 },21};2223export default route;
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
orquery
: validates the querystring. This can be a complete JSON Schema object, with the propertytype
ofobject
andproperties
object of parameters, or simply the values of what would be contained in theproperties
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 siblingHEAD
route for anyGET
routes. Defaults to the value ofexposeHeadRoutes
instance option. If you want a customHEAD
handler without disabling this option, make sure to define it before theGET
route.attachValidation
: attachvalidationError
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 sharedpreValidation
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 tothis
when the handler is called. Note: using an arrow function will break the binding ofthis
.errorHandler(error, request, reply)
: a custom error handler for the scope of the request. Overrides the default error global handler, and anything set bysetErrorHandler
in plugins, for requests to the route. To access the default handler, you can accessinstance.errorHandler
. Note that this will point to fastify's defaulterrorHandler
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 bysetSchemaErrorFormatter
, 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 withfastify(options)
. Defaults to1048576
(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 key | Description |
---|---|
request | the Request object describing the incoming HTTP request |
reply | the 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 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 to see what each connection provides. |
logger | a 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:
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply, api }) => {4 const records = await api.someModel.findMany();5 await reply.code(200).send({ result: records });6};78export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply, api }) => {4 const records = await api.someModel.findMany();5 await reply.code(200).send({ result: records });6};78export default route;
We can use the logger
object to emit log statements about our request:
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply, logger }) => {4 if (request.headers["authorization"] == "Bearer secret-token") {5 logger.info({ ip: request.ip }, "access granted");6 await reply.code(200).send({ result: "the protected stuff" });7 } else {8 logger.info({ ip: request.ip }, "access denied");9 await reply.code(403).send({ error: "access denied" });10 }11};1213export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply, logger }) => {4 if (request.headers["authorization"] == "Bearer secret-token") {5 logger.info({ ip: request.ip }, "access granted");6 await reply.code(200).send({ result: "the protected stuff" });7 } else {8 logger.info({ ip: request.ip }, "access denied");9 await reply.code(403).send({ error: "access denied" });10 }11};1213export 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:
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply, connections }) => {4 // relies on this request being made from an embedded app to know what `shopify.current` is5 const result = await connections.shopify.current.shop.get();6 await reply.code(200).send({ result });7};89export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply, connections }) => {4 // relies on this request being made from an embedded app to know what `shopify.current` is5 const result = await connections.shopify.current.shop.get();6 await reply.code(200).send({ result });7};89export default route;
Here's a full example demonstrating many of the elements of the request context:
1import type { RouteHandler } from "gadget-server";23/**4 * An example API route which finds a product for the current shopify shop, updates a model counter, and sends an SMS notification5 **/6const route: RouteHandler<{7 Body: {8 productId: string;9 productDescription: string;10 };11}> = async ({12 request,13 reply,14 api,15 applicationSession,16 connections,17 logger,18 config,19}) => {20 // use "applicationSession" to the get current shopId21 const shopId = applicationSession?.get("shop");2223 // if there's a current in context shopify client let's continue24 if (connections.shopify.current) {25 const product = await api.shopifyProduct.findById(request.body.productId);2627 // if the product belongs to the shop then update product with description from body28 if (product.get("shop") == shopId) {29 await connections.shopify.current.product.update(request.body.productId, {30 body: request.body.productDescription,31 });3233 // update count of updated products34 const updatedProductRecord = await api.updatedProduct.findFirst({35 filter: {36 shopId: {37 equals: shopId,38 },39 },40 });41 // atomically increment the number of products we've updated in our updatedProduct model42 await api.internal.updatedProduct.update(updatedProductRecord.id, {43 _atomics: {44 count: {45 increment: 1,46 },47 },48 });4950 // notify me via sms51 const twilio = require("twilio");52 // use "config" to pass along environment variables required by Twilio53 const client = new twilio(config.accountSid, config.authToken);5455 await client.messages.create({56 body: "Product has been updated!",57 to: "+12345678901",58 from: "+12345678901",59 });6061 logger.info(62 { productId: request.body.productId, shopId },63 "a product has been updated"64 );6566 await reply.send({ status: "ok" });67 } else {68 // couldn't find the product!69 await reply.status(404).send();70 }71 } else {72 // oops not authorized73 await reply.status(401).send();74 }75};7677export default route;
1import type { RouteHandler } from "gadget-server";23/**4 * An example API route which finds a product for the current shopify shop, updates a model counter, and sends an SMS notification5 **/6const route: RouteHandler<{7 Body: {8 productId: string;9 productDescription: string;10 };11}> = async ({12 request,13 reply,14 api,15 applicationSession,16 connections,17 logger,18 config,19}) => {20 // use "applicationSession" to the get current shopId21 const shopId = applicationSession?.get("shop");2223 // if there's a current in context shopify client let's continue24 if (connections.shopify.current) {25 const product = await api.shopifyProduct.findById(request.body.productId);2627 // if the product belongs to the shop then update product with description from body28 if (product.get("shop") == shopId) {29 await connections.shopify.current.product.update(request.body.productId, {30 body: request.body.productDescription,31 });3233 // update count of updated products34 const updatedProductRecord = await api.updatedProduct.findFirst({35 filter: {36 shopId: {37 equals: shopId,38 },39 },40 });41 // atomically increment the number of products we've updated in our updatedProduct model42 await api.internal.updatedProduct.update(updatedProductRecord.id, {43 _atomics: {44 count: {45 increment: 1,46 },47 },48 });4950 // notify me via sms51 const twilio = require("twilio");52 // use "config" to pass along environment variables required by Twilio53 const client = new twilio(config.accountSid, config.authToken);5455 await client.messages.create({56 body: "Product has been updated!",57 to: "+12345678901",58 from: "+12345678901",59 });6061 logger.info(62 { productId: request.body.productId, shopId },63 "a product has been updated"64 );6566 await reply.send({ status: "ok" });67 } else {68 // couldn't find the product!69 await reply.status(404).send();70 }71 } else {72 // oops not authorized73 await reply.status(401).send();74 }75};7677export 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 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 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 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 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 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.
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply.send({ foo: "bar" });5};67export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply.send({ foo: "bar" });5};67export 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 toapplication/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
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;
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:
1import { Readable } from "stream";2import { setTimeout } from "timers/promises";3import type { RouteHandler } from "gadget-server";45const route: RouteHandler = async ({ request, reply }) => {6 // create a stream object7 const stream = new Readable({8 read() {},9 encoding: "utf8",10 });1112 // start sending the stream to the client13 void reply.type("text/plain").send(stream);1415 // push the current time to the stream every second for 10 seconds16 let counter = 0;17 while (counter++ < 10) {18 stream.push(`${new Date()}\n`);19 await setTimeout(1000);20 }2122 // end the stream23 stream.push(null);24};2526export default route;
1import { Readable } from "stream";2import { setTimeout } from "timers/promises";3import type { RouteHandler } from "gadget-server";45const route: RouteHandler = async ({ request, reply }) => {6 // create a stream object7 const stream = new Readable({8 read() {},9 encoding: "utf8",10 });1112 // start sending the stream to the client13 void reply.type("text/plain").send(stream);1415 // push the current time to the stream every second for 10 seconds16 let counter = 0;17 while (counter++ < 10) {18 stream.push(`${new Date()}\n`);19 await setTimeout(1000);20 }2122 // end the stream23 stream.push(null);24};2526export 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 is installed.
1import { openAIResponseStream } from "gadget-server/ai";2import type { RouteHandler } from "gadget-server";34const route: RouteHandler = async ({ request, reply, connections }) => {5 const stream = await connections.openai.chat.completions.create({6 model: "gpt-3.5-turbo",7 messages: [{ role: "user", content: "Hello!" }],8 stream: true,9 });1011 await reply.send(openAIResponseStream(stream));12};1314export default route;
1import { openAIResponseStream } from "gadget-server/ai";2import type { RouteHandler } from "gadget-server";34const route: RouteHandler = async ({ request, reply, connections }) => {5 const stream = await connections.openai.chat.completions.create({6 model: "gpt-3.5-turbo",7 messages: [{ role: "user", content: "Hello!" }],8 stream: true,9 });1011 await reply.send(openAIResponseStream(stream));12};1314export 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.
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 const buffer = Buffer.from("hello world");5 await reply.type("text/plain").send(buffer);6};78export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 const buffer = Buffer.from("hello world");5 await reply.type("text/plain").send(buffer);6};78export 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:
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply5 .header("Cache-Control", "public, max-age=3600")6 .send("this response will be cached for 1 hour");7};89export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply5 .header("Cache-Control", "public, max-age=3600")6 .send("this response will be cached for 1 hour");7};89export default route;
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
.
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply5 .header("Cache-Control", "public, max-age=3600")6 .send("this will be cached for 1 hour");7};89export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply5 .header("Cache-Control", "public, max-age=3600")6 .send("this will be cached for 1 hour");7};89export 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.
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply5 .header("Cache-Control", "public, immutable, max-age=31536000")6 .send("this will be cached for as long as possible in the CDN and in browsers");7};89export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply5 .header("Cache-Control", "public, immutable, max-age=31536000")6 .send("this will be cached for as long as possible in the CDN and in browsers");7};89export 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.
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply5 .header("Cache-Control", "private, no-store")6 .send("this will never be cached by the CDN or browser");7};89export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply5 .header("Cache-Control", "private, no-store")6 .send("this will never be cached by the CDN or browser");7};89export 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.
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply5 .header("Cache-Control", "max-age=1, stale-while-revalidate=59")6 .send(7 "this will be served to users up to one minute out of date, and re-fetched in the background."8 );9};1011export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply5 .header("Cache-Control", "max-age=1, stale-while-revalidate=59")6 .send(7 "this will be served to users up to one minute out of date, and re-fetched in the background."8 );9};1011export default route;
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:
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply, api }) => {4 const posts = await api.post.findMany({5 sort: { createdAt: "Descending" },6 first: 10,7 });8 await reply.header("cache-control", "public, max-age=86400").send({ posts });9};1011export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply, api }) => {4 const posts = await api.post.findMany({5 sort: { createdAt: "Descending" },6 first: 10,7 });8 await reply.header("cache-control", "public, max-age=86400").send({ posts });9};1011export 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) 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.
Open your Gadget command palette ( P or Ctrl P) and run the following to install the package:
terminalyarn add @fastify/[email protected]
You can also manually add @fastify/cors
to your package.json
and click Run yarn to install it:
package.jsonjson1{2 "dependencies": {3 "@fastify/cors": "^9.0.1"4 // ...5 }6}
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
:
1import cors from "@fastify/cors";2import type { Server } from "gadget-server";34export default async function (server: Server) {5 await server.register(cors, {6 // allow CORS requests from my-cool-frontend.com7 origin: ["https://my-cool-frontend.com"],8 // allow GET, POST, and PUT requests9 methods: ["GET", "POST", "PUT"],10 // other options, see here: https://www.npmjs.com/package/fastify-cors11 });12}
1import cors from "@fastify/cors";2import type { Server } from "gadget-server";34export default async function (server: Server) {5 await server.register(cors, {6 // allow CORS requests from my-cool-frontend.com7 origin: ["https://my-cool-frontend.com"],8 // allow GET, POST, and PUT requests9 methods: ["GET", "POST", "PUT"],10 // other options, see here: https://www.npmjs.com/package/fastify-cors11 });12}
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.
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.jsonjson1{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
.
1import cors from "@fastify/cors";2import type { Server } from "gadget-server";34export default async function (server: Server) {5 await server.register(cors, {6 // allow requests from any domain7 origin: true,8 });9}
1import cors from "@fastify/cors";2import type { Server } from "gadget-server";34export default async function (server: Server) {5 await server.register(cors, {6 // allow requests from any domain7 origin: true,8 });9}
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:
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply5 .headers({6 // set reply headers for CORS explicitly7 "access-control-allow-origin": "*",8 "access-control-allow-methods": "POST, GET, OPTIONS",9 "access-control-allow-headers": "Content-Type",10 })11 .send("foo");12};1314export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 await reply5 .headers({6 // set reply headers for CORS explicitly7 "access-control-allow-origin": "*",8 "access-control-allow-methods": "POST, GET, OPTIONS",9 "access-control-allow-headers": "Content-Type",10 })11 .send("foo");12};1314export default route;
Or for cross-origin requests which require preflight requests, we can handle these requests explicitly by adding an OPTIONS-foo.js
route handler:
1// OPTIONS route handler for an adjacent routes/PUT-foo.ts route file2import type { RouteHandler } from "gadget-server";34const route: RouteHandler = async ({ request, reply }) => {5 await reply6 .headers({7 // set reply headers for CORS preflight request8 "access-control-allow-origin": "*",9 "access-control-allow-methods": "PUT, GET, OPTIONS",10 "access-control-allow-headers": "Content-Type",11 })12 .send(13 // send no response to meet the preflight request spec14 ""15 )16 .code(200);17};1819export default route;
1// OPTIONS route handler for an adjacent routes/PUT-foo.ts route file2import type { RouteHandler } from "gadget-server";34const route: RouteHandler = async ({ request, reply }) => {5 await reply6 .headers({7 // set reply headers for CORS preflight request8 "access-control-allow-origin": "*",9 "access-control-allow-methods": "PUT, GET, OPTIONS",10 "access-control-allow-headers": "Content-Type",11 })12 .send(13 // send no response to meet the preflight request spec14 ""15 )16 .code(200);17};1819export default route;
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.jsonjson{"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:
1import FastifyMultipart from "@fastify/multipart";2import type { Server } from "gadget-server";34export default async function (server: Server) {5 await server.register(FastifyMultipart);6}
1import FastifyMultipart from "@fastify/multipart";2import type { Server } from "gadget-server";34export default async function (server: Server) {5 await server.register(FastifyMultipart);6}
With the plugin registered, you can access POSTed file data using request.file
or request.files
in your routes:
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 // process a single file5 const data = await request.file();67 // the stream of file data, is a node ReadableStream object8 data.file;9 // other parsed parts of the multipart request10 data.fields;11 // the name of the key the file is stored under in the incoming data12 data.fieldname;13 // the name of the file if provided14 data.filename;15 // the file encoding16 data.encoding;17 // the mimetype inferred from the file using the extension18 data.mimetype;1920 // do something with the file21 reply.send({ status: "ok" });22};2324export default route;
1import type { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ request, reply }) => {4 // process a single file5 const data = await request.file();67 // the stream of file data, is a node ReadableStream object8 data.file;9 // other parsed parts of the multipart request10 data.fields;11 // the name of the key the file is stored under in the incoming data12 data.fieldname;13 // the name of the file if provided14 data.filename;15 // the file encoding16 data.encoding;17 // the mimetype inferred from the file using the extension18 data.mimetype;1920 // do something with the file21 reply.send({ status: "ok" });22};2324export default route;
For more information on working with @fastify/multipart
, see the @fastify/multipart
docs.
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.jsonjson1{2 "dependencies": {3 "fastify-multipart": "5.3.1"4 // ...5 }6}