So, you want to use Hydrogen, but you don't want to use Oxygen?! You silly scaggly-waggly. No worriesβwe can help.
Remix can be deployed to lots of different places. Just look at this list! Anywhere Remix can be deployed, Hydrogen can be deployed.
Rather than trying to provide you an exhaustive example for every single adapter today and in the future, let's focus on a fictional hosting runtime: Funkytown Hosting.
We'll pretend that you've already spun up a Hydrogen app using the shopify hydrogen
CLI, and you plan to convert it to Funkytown.
Let's walk through some situations you might encounter on your journey!
Remix hosting adapters take lots of different forms. This is to be expected! Vercel has a Build Output API, Cloudflare Pages has its own set of rules... The list goes on.
Tip: The absolute best way to start migrating to a different platform is to read the platform's docs, and to take a look at the hosting adapters Remix template.
Where do you find these hosting templates? Good question. Remix hosts them in its repo today, but that won't always be the case. Check your hosting platform's docs to see where the adapter's code lives.
Each platform will have its own constraints, including:
- The CLI they use to run a local development server
- Updating and accessing environment variables
- Caching: whether they provide a Cache API and how to access it
- Which headers are available to contextualize the visitor
All of these constraints ^ will affect how you set up your Hydrogen app, so you should take some time to familiarize yourself with these constraints in your hosting platform.
Let's pretend we got a heck of a deal on a Funkytown Hosting package, and Funkytown just so happens to offer a Remix adapter π
First things first: you read the docs, and it looks like Funkytown maintains their own Remix adapter at https://github.com/funkytown4eva/remix-adapter
and an example template at https://github.com/funkytown4eva/remix-template
. This adapter is available on NPM at @funkytown/remix
.
After familiarizing yourself with the Funkytown's hosting offerings, it's time to update our Hydrogen app to get it running on the blazingly-fast and fresh Funkytown hosting platform.
Let's dive in. Funkytown's docs tell us that we need to install the funkytown
CLI in order to run Funkytown projects locally. We confirm this when we see the use of the funkytown
in the package.json
scripts listed in the Funkytown Remix template hosted on GitHub.
So let's install the CLI globally:
npm i -g funkytown
We also need to install the Funkytown Remix adapter into our local Hydrogen app:
npm i @funkytown/remix
Yay! Now, we need to make sure our local package.json#scripts
are updated with the matching funkytown
commands seen in the Funkytown Remix template:
{
"scripts": {
"dev": "funkytown dev",
"build": "funkytown build",
"start": "funkytown start"
}
}
Every hosting platform needs to have a single entrypoint into your app to be able to execute it. In other adapters, you might see a server.js
or a worker.js
file as the entrypoint. Remix also needs to know about this entrypoint so it can run and bundle your app.
Funkytown just so happens to have their Super Duper Build APIβ’, and their CLI expects there to be a funkytown.js
file as an entrypoint in order to hook into their build process.
Let's take a look at the funkytown.js
file in the Funkytown Remix template:
import { createFunkytownHandler } from "@funkytown/remix";
import * as build from "@remix-run/dev/server-build";
const adapterMiddleware = async ({context}) => {
// Place any middleware here
};
const handleRequest = createFunkytownHandler({
build,
mode: process.env.NODE_ENV,
adapterMiddleware,
});
export function giveMeTheFunk(context) {
return handleRequest(context);
}
Baller π
In this template, Funkytown is importing a function called createFunkytownHandler
from the @funkytown/remix
package. This function is a wrapper around the Remix handleRequest
function, and it's what Funkytown uses to handle requests.
Funkytown's fancy build API expects its entrypoints to export a giveMeTheFunk
function to handle requests, so that's that is exported from this file.
Tip: You'll want to be sure to reference the
remix.config.js
in the hosting runtime's example Remix template, and update your localremix.config.js
to match.
But what's this adapterMiddleware
function? This is a function that allows you to add middleware to your app. This is a great place to inject platform-specific things like environment variables, caching, and more.
Yes, Middleware. It is π₯, and it's a native feature in Remix.
Middleware lives in Remix apps in a couple different places with varying degrees of functionality:
- In the hosting adapter entrypoint. This allows you to inject context, modify the incoming request header, and modify the outgoing request.
- In individual routes. This includes the
root.tsx
file, and any other route files you create. This allows you to read and update context, but you CANNOT modify the outgoing response.
Hydrogen apps are powered by a storefrontClient
that is injected into context in the root
middleware. Let's take a look at the middleware in a boilerplate Hydrogen app which is designed to be hosted on Oxygen:
export async function middleware({context, request}: MiddlewareArgs) {
const {waitUntil, cache, env} = context.get(oxygenContext);
const session = await HydrogenSession.init(request, [env.SESSION_SECRET]);
/**
* Create Hydrogen's Storefront client.
*/
const {storefront} = createStorefrontClient<I18nLocale>({
cache,
waitUntil,
buyerIp: getBuyerIp(request),
i18n: getLocaleFromRequest(request),
publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,
privateStorefrontToken: env.PRIVATE_STOREFRONT_API_TOKEN,
storeDomain: `https://${env.PUBLIC_STORE_DOMAIN}`,
storefrontApiVersion: env.PUBLIC_STOREFRONT_API_VERSION || '2023-01',
storefrontId: env.PUBLIC_STOREFRONT_ID,
requestGroupId: request.headers.get('request-id'),
});
// Set helpful context for route loaders, actions, and middleware
context.set(hydrogenContext, {
storefront,
session,
waitUntil,
});
const response = await context.next();
if (response.status === 404) {
/**
* Check for redirects only when there's a 404 from the app.
* If the redirect doesn't exist, then `storefrontRedirect`
* will pass through the 404 response.
*/
return await storefrontRedirect({request, response, storefront});
}
return response;
}
Woof, that's a lot. But it's important!
Let's take a look at a few key things:
const {waitUntil, cache, env} = context.get(oxygenContext);
We pull waitUntil
, cache
and env
objects out of a middleware context called oxygenContext
.
As you might have guessed, these values are Oxygen-specific. This is necessary because Oxygen's hosting runtime injects utilities like cache
(for caching) and env
(which contains the app's environment variables set through the Oxygen UI) inside the hosting entrypoint (which happens to be server.js
in this case).
We need some of these things in order to create the Hydrogen storefront client:
cache
is used for the underlying Storefront API requests.waitUntil
is used to ensure we can populate the cache after the response has been returned.env
is used to access the Storefront API tokens and more.
So what are the Funkytown equivalent to these values?
- Funkytown happens to provide a built-in
cache
object which adheres to the Cache API. - Funkytown doesn't require you to use
waitUntil
, so we can ignore that option. - Funkytown injects a
context.myFunkyEnv
object into the request handler which contains your environment variables that you've set using thefunkytown env
command (or through Funkytown's UI).
Tip: If your hosting runtime doesn't provide a
Cache
API out of the box, you can still run your Hydrogen app. Just know that sub-requests to the Storefront API will not be cached, regardless of whether you're applyingcache
headers.
We need to update our funkytown.js
entrypoint to update the adapter middleware so that we inject the correct context into our Hydrogen app, so that we can read the cache
and env
values later on in the root
middleware.
To start, we'll want to create a new MiddlewareContext
for Funkytown. Let's create one in app/context.js
and call it funkytownContext
:
// app/context.js
import {createMiddlewareContext} from '@funkytown/remix';
export const funkytownContext = createMiddlewareContext();
Now, let's update our funkytown.js
entrypoint to inject the funkytownContext
into the adapterMiddleware
function:
+import { funkytownContext } from '~/context';
const adapterMiddleware = async ({context}) => {
+ context.set(funkytownContext, {
+ cache: context.cache,
+ env: context.myFunkyEnv,
+ });
};
Cool. Now we can access the cache
and env
values in our root
middleware:
export async function middleware({context, request}: MiddlewareArgs) {
- const {waitUntil, cache, env} = context.get(oxygenContext);
+ const {cache, env} = context.get(funkytownContext);
const session = await HydrogenSession.init(request, [env.SESSION_SECRET]);
/**
* Create Hydrogen's Storefront client.
*/
const {storefront} = createStorefrontClient<I18nLocale>({
cache,
- waitUntil,
buyerIp: getBuyerIp(request),
i18n: getLocaleFromRequest(request),
publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,
privateStorefrontToken: env.PRIVATE_STOREFRONT_API_TOKEN,
storeDomain: `https://${env.PUBLIC_STORE_DOMAIN}`,
storefrontApiVersion: env.PUBLIC_STOREFRONT_API_VERSION || '2023-01',
storefrontId: env.PUBLIC_STOREFRONT_ID,
requestGroupId: request.headers.get('request-id'),
});
// Set helpful context for route loaders, actions, and middleware
context.set(hydrogenContext, {
storefront,
session,
waitUntil,
});
const response = await context.next();
if (response.status === 404) {
/**
* Check for redirects only when there's a 404 from the app.
* If the redirect doesn't exist, then `storefrontRedirect`
* will pass through the 404 response.
*/
return await storefrontRedirect({request, response, storefront});
}
return response;
}
OK, getting closer. But hold-up: what's the deal with getBuyerIp()
?
Shopify uses Buyer IP addresses to help mitigate rate limiting when proxying Storefront API requests across the server. It's important for you to provide your current buyer's IP address to the Storefront API so that you don't get rate limited. Read more about this here.
However, buyer or visitor IP addresses will be distributed differently based on your hosting runtime! Usually, it's included as a request header. Some hosts will provide it as x-forwarded-for
. Others, like Oxygen, will provide it as a namespaced header for the hosting runtime like x-oxygen-buyer-ip
.
In our case, we check Funkytown's docs and find out that Funkytown injects a funky-monkey-ip
header that contains the user's IP address. Let's use that, and ditch the Oxygen-specific getBuyerIp()
function:
export async function middleware({context, request}: MiddlewareArgs) {
const {cache, env} = context.get(funkytownContext);
const session = await HydrogenSession.init(request, [env.SESSION_SECRET]);
/**
* Create Hydrogen's Storefront client.
*/
const {storefront} = createStorefrontClient<I18nLocale>({
cache,
- buyerIp: getBuyerIp(request),
+ buyerIp: request.headers.get('funky-monkey-ip'),
i18n: getLocaleFromRequest(request),
publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,
privateStorefrontToken: env.PRIVATE_STOREFRONT_API_TOKEN,
storeDomain: `https://${env.PUBLIC_STORE_DOMAIN}`,
storefrontApiVersion: env.PUBLIC_STOREFRONT_API_VERSION || '2023-01',
storefrontId: env.PUBLIC_STOREFRONT_ID,
requestGroupId: request.headers.get('request-id'),
});
// Set helpful context for route loaders, actions, and middleware
context.set(hydrogenContext, {
storefront,
session,
waitUntil,
});
const response = await context.next();
if (response.status === 404) {
/**
* Check for redirects only when there's a 404 from the app.
* If the redirect doesn't exist, then `storefrontRedirect`
* will pass through the 404 response.
*/
return await storefrontRedirect({request, response, storefront});
}
return response;
}
Cool, cool. One more more thing to look at here:
requestGroupId: request.headers.get('request-id')
The request-id
is a unique ID that Oxygen injects into every request for a Hydrogen app, and Hydrogen's storefront client uses it to "tag" each sub-request to the Storefront API. Shopify uses this information to be able to diagnose issues with a response to a Hydrogen app, like the number of sub-requests made, and more.
What does Funkytown provide in terms of unique Request ID? Funkytown docs to the rescue π It looks like Funkytown injects a x-funky-request-id
header that contains a unique ID for each request. Let's use that instead:
-requestGroupId: request.headers.get('request-id')
+requestGroupId: request.headers.get('x-funky-request-id')
I think we're ready. By this point, you have:
- Installed Funkytown's Remix adapter and CLI tool
- Updated your
package.json
scripts andremix.config.js
to use the same values Funkytown uses in their template - Added a
funkytown.js
entrypoint to your app, including theadapterMiddleware
function which injects Funkytown-specific context into your Hydrogen app - Updated the
root.tsx
middleware to use thefunkytownContext
instead of theoxygenContext
, and update the arguments tocreateStorefrontClient
to use these values.
Let's give it a shot:
npm run dev
π Everything works!
Now that you've confirmed everything works in Funkytown, you're gonna want to remove Oxygen stuff from your local repository, including:
- The
@shopify/remix-oxygen
dependency - The
@shopify/cli-hydrogen
dependency (it's only useful for Oxygen local development) - The old
server.js
entrypoint used by Oxygen - Any stray references to the
oxygenContext
used by middleware
Happy coding!
ring ring
YOU: "Hello?"
JANE: "Yeah, it's me, Jane, your CTO."
YOU: "What's up, Jane?"
JANE: "So, we just spun up a few Redis clusters across the globe, and I think you should use these for caching instead of Funkytown's built-in Cache APIs."
YOU: "Oh, yeah?"
JANE: "Yeah. We like to have control over our caching strategy, and we think we can net some performance wins by not being restricted to Funkytown's cache which is isolated to each region."
YOU: "On it! Jane, you're the best!"
OK, let's have some fun. We start by opening up our funkytown.js
entrypoint and adding a new cache
implementation:
import {redis} from 'some-3p-redis-client';
class FunkytownCache implements Cache {
async function get(key: string) {
// return await redis.get(key);
}
async function set(key: string, value: string) {
// return await redis.set(key, value);
}
// ...
}
const cache = new FunkytownCache();
export async function adapterMiddleware({context, request}: MiddlewareArgs) {
// ...
context.set(funkytownContext, {
cache,
// ...
});
}
Now your Hydrogen app uses the Redis-based cache instead!
More and more hosting runtimes are implementing a fetch-based cache by monkeypatching the fetch
API. This works by setting additional information on the RequestInit
object of fetch
.
Let's pretend that Funkytown has implemented this cache API:
fetch('https://example.com', {
funkytown: {
cache: 'max-age=3600',
}
});
Hydrogen allows you to hook into storefront.query
to pass RequestInit
values. To use this, you'll want to update all references to storefront.query
to pass your new Funkytown cache directives:
await storefront.query(QUERY, VARIABLES, {
init: {
funkytown: {
cache: 'max-age=3600',
}
}
})
Now, all sub-requests made to the Storefront API will be cached for 1 hour using Funkytown's fetch-based cache API!
<3 this. It's crazy how much easier to follow this is than regular docs