Editor's Note: In this post, freelance developer Joey Anuff explores how to pair a Remix app with a StepZen GraphQL endpoint. For the full code, visit Joey's GitHub repo at januff/stepzen-spotify-knowledge.

Two Next.js choices that always make me a little nervous: choosing a pre-rendering strategy (server or static or incremental?) and choosing a data hooks library (Apollo or react-query or urql?) So when a new web framework like Remix emerges, suggesting I might be better off choosing neither, any excuse to install it will do.

Happily, some of DevRel’s craftiest React specialists must have been feeling the same way, handing me everything I need to answer the question: what’s the simplest pairing of a Remix app and a StepZen endpoint?

Remix Data

Which is not to say Remix’s own docs aren’t sufficiently instructive–besides their ecosystem credentials, the Remix team shares long stints in React training, which pays off in carefully illuminating site docs and YouTube nuggets. But as far as I could tell, they’d not yet put forth guidance on my specific question: how best to hook up a GraphQL endpoint to a Remix app, preferably with no external dependencies.

But with a few tricks culled from recent tutorials by GraphQL blogger Jamie Barton, JS live-streamer Jason Lengstorf, and advanced FE YouTuber Jack Herrington, I was surprised to learn how little time and code it takes to make that happen.

Trick 1: Plug in any GraphQL endpoint

Jamie Barton I credit with the basic template I’m swiping here: Working with Remix, GraphQL, and GraphCMS, a simplest-possible GraphQL-driven Remix project with params-driven route loading. Like StepZen, GraphCMS provides developers with a single GraphQL endpoint, ensuring that instructions for hooking up one GraphQL service work as instructions for hooking up all GraphQL services.

npm install graphql-request graphql

Jamie's quickstart is perfectly replicable as is, but rather than import Prisma Lab's graphql-request client as he does, it seemed fitting–given Remix's hard preference for platform APIs–to try refactoring it to use the Web Fetch API (which Remix already polyfills on the server.)

Trick 2: Use the basic Web Fetch API for GraphQL

Towards the end of Jason Lengstorf’s highly recommended framework walkthrough with Remix co-creator Ryan Florence, he reminds us of the simple pattern for querying GraphQL endpoints using the Fetch API, which he’d detailed earlier as a no-nonsense how-to. (In short: specify a method, include headers, and JSON.stringify() the body.)

Adapted to my spotify_Search_With_Token query, a GraphQL Fetch from my index page loader looks like this:

export const loader = ({request}) => {
  const url = new URL(request.url)
  const search = url.searchParams.get('search') ?? 'Beatles Norwegian Wood'
  return getStepzen(search)

export async function getStepzen(search: string) {
  let res = await fetch(`${process.env.STEPZEN_ENDPOINT}`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `${process.env.STEPZEN_API_KEY}`,
    body: JSON.stringify({
      query: `
        query MyQuery($query: String!) {
          spotify_Search_With_Token(q: $query) {
            artistsInfo {
              detailedDescription {
            trackInfo {
              detailedDescription {
            albumInfo {
              detailedDescription {
      variables: {
        query: search,
  return res.json()

Whether the syntax of graphql-request makes such a query any more readable, you'll have to judge for yourself. (I find them equally comprehensible, I just dig fewer imports.)

Trick 3: Use Remix’s useSearchParams to track search query state

As far as building a simple search-input demo, I found Jack Herrington’s Pokemon-themed speed-run on YouTube to provide a slightly easier template than Kent C. Dodd’s search-input example, particularly Jack’s post-tutorial insertion of Remix’s useSearchParams hook, which he references in a pinned YouTube comment.

export default function Index() {
  const { spotify_Search_With_Token: song } = useLoaderData().data;
  const [search, setSearch] = useState(useSearchParams()[0].get("search") ?? "");

  return (
    <div className="remix-stepzen">
        <h3>Remix, GraphQL, and StepZen</h3>
      <Form method="get" className="search-form">
          placeholder="Band & Song..."
        <button type="submit">Spotify + Knowledge</button>
        { song ? (
          <div className="song-info">
            <h5>{song.trackInfo[0]?.detailedDescription?.articleBody || song.trackInfo[0]?.description || ""}</h5>
            <h5>{song.artistsInfo[0]?.detailedDescription?.articleBody || song.artistsInfo[0]?.description || ""}</h5>
            <h5>{song.albumInfo[0]?.detailedDescription?.articleBody || song.albumInfo[0]?.description || ""}</h5>
        )  : <h4>No Results</h4> }

Using this approach, defining separate params-based routes isn't even necessary: the search params in the URL already specify a route.


Mr. Herrington’s video helpfully zips through a Tailwind installation (also well-explained on the Tailwind site) but I've opted to keep my CSS simple for now, at least while I'm still working on my Remix fundamentals.