Common use cases for HTTP routes 

The most common use cases for HTTP routes in Gadget are:

  • Shopify HMAC validation
  • 3rd party webhook handling
  • AI streaming

HMAC validation 

When making calls to your Gadget backend from a Shopify storefront it is a good idea to set up a Shopify app proxy to make sure that requests are being made to your routes in a secure manner.

Note that a valid HMAC signature doesn't set the session to an authenticated role. This means that making a POST request to your GraphQL API would be using the unauthenticated role.

Once your Shopify app proxy is set up, you can make proxied requests from Shopify Liquid code to your Gadget backend and have context to the current shop making the request. This is possible because the Gadget platform handles validating the HMAC signature, sent in the request query parameters, for you.

In any proxied, route you'll have the connections.shopify.current context, allowing you to make authenticated requests to the Shopify API.

api/routes/proxy/POST-my-protected-route.js
JavaScript
1import { RouteHandler } from "gadget-server";
2
3/**
4 * Route handler for POST proxy
5 *
6 * See: https://docs.gadget.dev/guides/http-routes/route-configuration#route-context
7 */
8const route: RouteHandler = async ({ request, reply, api, logger, connections }) => {
9 const shopify = connections.shopify.current;
10
11 // Example of calling the Shopify API
12 const response = await shopify.graphql(
13 `
14 mutation ($metafields: [MetafieldsSetInput!]!) {
15 metafieldsSet(metafields: $metafields) {
16 metafields {
17 key
18 namespace
19 value
20 }
21 userErrors {
22 message
23 }
24 }
25 }
26 `,
27 {
28 metafields: [
29 {
30 key: "nickname",
31 namespace: "my_fields",
32 ownerId: "gid://shopify/Customer/624407574",
33 type: "single_line_text_field",
34 value: "Big Tuna",
35 },
36 ],
37 }
38 );
39
40 if (response?.metafieldsSet?.userErrors?.length) {
41 logger.error(
42 { errors: response.metafieldsSet.userErrors },
43 "Errored setting metafields"
44 );
45 return await reply.code(500).send();
46 }
47
48 await reply.send();
49};
50
51export default route;
1import { RouteHandler } from "gadget-server";
2
3/**
4 * Route handler for POST proxy
5 *
6 * See: https://docs.gadget.dev/guides/http-routes/route-configuration#route-context
7 */
8const route: RouteHandler = async ({ request, reply, api, logger, connections }) => {
9 const shopify = connections.shopify.current;
10
11 // Example of calling the Shopify API
12 const response = await shopify.graphql(
13 `
14 mutation ($metafields: [MetafieldsSetInput!]!) {
15 metafieldsSet(metafields: $metafields) {
16 metafields {
17 key
18 namespace
19 value
20 }
21 userErrors {
22 message
23 }
24 }
25 }
26 `,
27 {
28 metafields: [
29 {
30 key: "nickname",
31 namespace: "my_fields",
32 ownerId: "gid://shopify/Customer/624407574",
33 type: "single_line_text_field",
34 value: "Big Tuna",
35 },
36 ],
37 }
38 );
39
40 if (response?.metafieldsSet?.userErrors?.length) {
41 logger.error(
42 { errors: response.metafieldsSet.userErrors },
43 "Errored setting metafields"
44 );
45 return await reply.code(500).send();
46 }
47
48 await reply.send();
49};
50
51export default route;

If a customer is logged in to the Shopify store, Shopify sends logged_in_customer_id in the request's query params. You can then use this to fetch or update customer data in your Gadget backend.

api/routes/proxy/GET-my-protected-route.js
JavaScript
1import { RouteHandler } from "gadget-server";
2
3/**
4 * Route handler for GET proxy
5 *
6 * See: https://docs.gadget.dev/guides/http-routes/route-configuration#route-context
7 */
8const route: RouteHandler = async ({ request, reply, api, logger, connections }) => {
9 const customerId = request.query.logged_in_customer_id;
10
11 // This can also be done in an `onRequest` hook
12 if (!customerId) {
13 return await reply.code(401).send({ error: { message: "Unauthorized" } });
14 }
15
16 const customer = await api.shopifyCustomer.maybeFindOne(customerId, {
17 select: {
18 customData: true,
19 // ... other fields
20 },
21 });
22
23 if (!customer)
24 return await reply.code(404).send({ error: { message: "Customer not found" } });
25
26 await reply.send({
27 ...customer,
28 });
29};
30
31export default route;
1import { RouteHandler } from "gadget-server";
2
3/**
4 * Route handler for GET proxy
5 *
6 * See: https://docs.gadget.dev/guides/http-routes/route-configuration#route-context
7 */
8const route: RouteHandler = async ({ request, reply, api, logger, connections }) => {
9 const customerId = request.query.logged_in_customer_id;
10
11 // This can also be done in an `onRequest` hook
12 if (!customerId) {
13 return await reply.code(401).send({ error: { message: "Unauthorized" } });
14 }
15
16 const customer = await api.shopifyCustomer.maybeFindOne(customerId, {
17 select: {
18 customData: true,
19 // ... other fields
20 },
21 });
22
23 if (!customer)
24 return await reply.code(404).send({ error: { message: "Customer not found" } });
25
26 await reply.send({
27 ...customer,
28 });
29};
30
31export default route;

For a full working example, we recommend that you fork the product-quiz-template application and complete its setup steps.

Fork on Gadget

3rd party webhook handling 

When you need to listen to webhooks from a service that Gadget does not have a connection for, you can use an HTTP route to listen to the webhook and trigger your logic. For this example, we'll use GitHub's webhook service to listen to star events on a repository.

An example route file for this use case would look like this:

api/routes/POST-github-webhook.js
JavaScript
1import { RouteHandler } from "gadget-server";
2
3const route: RouteHandler = async ({ request, reply, logger }) => {
4 // write the incoming webhook payload to the logs
5 logger.debug({ request }, "log the incoming webhook request");
6 // reply with a 204 response
7 return await reply.code(204).send();
8};
9
10export default route;
1import { RouteHandler } from "gadget-server";
2
3const route: RouteHandler = async ({ request, reply, logger }) => {
4 // write the incoming webhook payload to the logs
5 logger.debug({ request }, "log the incoming webhook request");
6 // reply with a 204 response
7 return await reply.code(204).send();
8};
9
10export default route;

Now you can hook up this new route to a webhook subscription. For this example, we'll use GitHub's star events webhook. To subscribe to this webhook:

  • Log into your GitHub account and navigate to one of your repositories or create a new, empty repo
  • Click on Settings --> Webhooks --> Add webhook
  • In the Payload URL field, enter the URL for your new Gadget route
  • In the Content type field, select application/json
  • In the Which events would you like to trigger this webhook? section, select Let me select individual events. and then select Stars from the list of events. Uncheck the default Pushes event.
  • Click Add webhook to save your webhook subscription
Screenshot of the webhook configuration page in GitHub, with a Payload URL entered, the 'Content type' set to 'application/json', and the 'Let me select individual events.' option selected.

That's all that is required, GitHub will send a webhook to your Gadget route! You can test this by starring and unstarring your repository and then checking your app's logs.

AI response streaming 

You can use routes to stream responses from AI. Let's say that you have a chatbot and want users to be able to see the response as it's being generated. For this example we'll use OpenAI's GPT-4 model to generate responses to chat messages.

This example is partially taken out of our chatgpt-template. If you'd like to take a look at a working example, I would recommend forking the application with this link.

Fork on Gadget

You would start by creating a route that accepts a POST request with chat and message data. Here's an example of a POST-chat route:

api/routes/POST-chat.js
JavaScript
1import { RouteHandler } from "gadget-server";
2import { openAIResponseStream } from "gadget-server/ai";
3import type { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions";
4
5type ChatRequestBody = {
6 chatId: string;
7 messages: ChatCompletionCreateParamsBase["messages"];
8};
9
10const route: RouteHandler = async ({ request, reply, api, logger, connections }) => {
11 const { chatId, messages } = request.body as ChatRequestBody;
12
13 // log the incoming chat request
14 logger.debug({ chatId, messages }, "creating new chat completion");
15
16 const openAIResponse = await connections.openai.chat.completions.create({
17 model: "gpt-4-1106-preview",
18 messages,
19 stream: true,
20 });
21
22 const onComplete = (content: string | null | undefined) => {
23 // Save the chat history
24 void api.message.create({
25 order: messages.length + 1,
26 role: "assistant",
27 content,
28 chat: {
29 _link: chatId,
30 },
31 });
32 };
33
34 await reply.send(openAIResponseStream(openAIResponse, { onComplete }));
35};
36
37export default route;
1import { RouteHandler } from "gadget-server";
2import { openAIResponseStream } from "gadget-server/ai";
3import type { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions";
4
5type ChatRequestBody = {
6 chatId: string;
7 messages: ChatCompletionCreateParamsBase["messages"];
8};
9
10const route: RouteHandler = async ({ request, reply, api, logger, connections }) => {
11 const { chatId, messages } = request.body as ChatRequestBody;
12
13 // log the incoming chat request
14 logger.debug({ chatId, messages }, "creating new chat completion");
15
16 const openAIResponse = await connections.openai.chat.completions.create({
17 model: "gpt-4-1106-preview",
18 messages,
19 stream: true,
20 });
21
22 const onComplete = (content: string | null | undefined) => {
23 // Save the chat history
24 void api.message.create({
25 order: messages.length + 1,
26 role: "assistant",
27 content,
28 chat: {
29 _link: chatId,
30 },
31 });
32 };
33
34 await reply.send(openAIResponseStream(openAIResponse, { onComplete }));
35};
36
37export default route;

The corresponding client-side code should look something like the following:

React
1import { useState } from "react";
2import { useFindMany, useFetch, useUser } from "@gadgetinc/react";
3import { Message, ChatGPTIcon } from "./components";
4import { api } from "../api";
5
6export const Chat = ({ id }) => {
7 const user = useUser();
8 const [message, setMessage] = useState("");
9
10 const [{ data: messages }] = useFindMany(api.message, { filter: { chat: { equals: id } } });
11 const [{ data: response, streaming: streamingResponse }, getResponse] = useFetch("/chat", {
12 method: "POST",
13 headers: { "content-type": "application/json" },
14 stream: "string",
15 });
16
17 return (
18 <div>
19 {messages.map((message) => (
20 <Message
21 key={message.id}
22 content={message.content}
23 role={message.role}
24 icon={message.role === "user" ? <img src={user.googleImageUrl || ""} /> : <ChatGPTIcon />}
25 />
26 ))}
27 {streamingResponse && <Message content={response} role={"assistant"} icon={<ChatGPTIcon />} />}
28 <form
29 onSubmit={(e) => {
30 e.preventDefault();
31 if (!message.trim()) return; // Prevent sending empty messages
32 getResponse({ chatId: id, messages: [{ role: "user", content: message }] });
33 setMessage(""); // Clear input after sending
34 }}
35 >
36 <input type="text" value={message} onChange={(e) => setMessage(e.target.value)} />
37 <button type="submit">Send</button>
38 </form>
39 </div>
40 );
41};
1import { useState } from "react";
2import { useFindMany, useFetch, useUser } from "@gadgetinc/react";
3import { Message, ChatGPTIcon } from "./components";
4import { api } from "../api";
5
6export const Chat = ({ id }) => {
7 const user = useUser();
8 const [message, setMessage] = useState("");
9
10 const [{ data: messages }] = useFindMany(api.message, { filter: { chat: { equals: id } } });
11 const [{ data: response, streaming: streamingResponse }, getResponse] = useFetch("/chat", {
12 method: "POST",
13 headers: { "content-type": "application/json" },
14 stream: "string",
15 });
16
17 return (
18 <div>
19 {messages.map((message) => (
20 <Message
21 key={message.id}
22 content={message.content}
23 role={message.role}
24 icon={message.role === "user" ? <img src={user.googleImageUrl || ""} /> : <ChatGPTIcon />}
25 />
26 ))}
27 {streamingResponse && <Message content={response} role={"assistant"} icon={<ChatGPTIcon />} />}
28 <form
29 onSubmit={(e) => {
30 e.preventDefault();
31 if (!message.trim()) return; // Prevent sending empty messages
32 getResponse({ chatId: id, messages: [{ role: "user", content: message }] });
33 setMessage(""); // Clear input after sending
34 }}
35 >
36 <input type="text" value={message} onChange={(e) => setMessage(e.target.value)} />
37 <button type="submit">Send</button>
38 </form>
39 </div>
40 );
41};

Was this page helpful?