Remix is a great React framework with a focus on server-side rendering. Remix allows applications to have a fast load time, and once the application is loaded, hydration kicks in and gives it client-side functionality.
Since Remix can run on the server, we can create API routes in our Remix application that can perform backend tasks, like connecting to a database. Thanks to technologies and tools like Apollo GraphQL, we can utilize Remix API routes to build a functional, full-stack GraphQL application.
In this article, we will cover how to set up a Remix application with GraphQL functionality. We’ll look into how we can implement simple CRUD functionality in a Remix application by creating GraphQL server routes with Apollo.
Remix is a full-stack web framework that focuses on the user interface. It works back through web fundamentals to deliver a fast, sleek, and resilient user experience.
Remix is built on React and includes React Router, server-side rendering, TypeScript support, production server, and backend optimization.
If you are familiar with React, you will know that there are several frameworks that offer server-side rendering capabilities built on React. A few such frameworks include Next.js and Astro.
Remix stands out from other server-side React frameworks for a few reasons. Firstly, unlike other frameworks like Next.js, it does not offer static site generation (SSG). Rather, it builds on the server/client model and focuses on SSR, building and compiling everything on the server and leveraging distributed systems at the edge. The client receives a smaller payload and is hydrated on the client side with React.
Remix also completely embraces web standards like the Web Fetch API, allowing developers to leverage the core tools and features the web has to offer and has developed over the years. For example, Remix leverages HTTP caching and lets the browser deal with any complexity of caching resources.
Finally, we’ll find that Remix leverages HTML <form>
when it comes to data mutations and CRUD functionality, unlike other frameworks. Then, it uses action
s and loader
s to handle requests sent with the <form>
.
With Remix, routes are their own APIs. Since everything is on the server, the component gets the data on the server side when the user requests that route.
Routes in Remix are another key difference between Remix and a framework like Next.js, where the client is required to make requests to the API/server routes to perform CRUD operations with the help of loader
s and action
s.
Take a look at the code below:
// ./app/routes/index.tsx import { json } from '@remix-run/node'; import { useLoaderData } from '@remix-run/react'; // type definitions type Book = { title: string; genre: string; }; type Books = Array<Book>; type LoaderData = { books: Books; }; // Loader function export const loader = async () => { return json<LoaderData>({ books: [ { title: 'Harry Potter and the Deathly Hallows', genre: "Children's Fiction", }, { title: "Harry Potter and the Philosopher's Stone", genre: "Children's Fiction", }, ], }); }; export default function Index() { // use data from loader const { books } = useLoaderData() as LoaderData; return ( <div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.4' }}> <h1>Welcome to Remix</h1> <ul> {books.map(({ title, genre }, i) => { return ( <li key={i}> <h3> {title} </h3> <p> {genre} </p> </li> ); })} </ul> </div> ); }
Click here to view on StackBlitz.
In the code above, we can see we declared a loader
to return an array of books that can be fetched from a remote server or database, but it’s hardcoded for now. Since loader
functions are the backend “API” for our routes, we can easily get the data from the loader
in our Index
component with the help of useLoaderData
.
Since this is all happening on the server, it is rendered and sent to the browser. There is no additional fetching done on the client side.
In addition to fetching data, we can also send data to be processed on the server side using actions
.
Let’s add a form with method="post"
to our Index
component and an action
that will handle the submission request:
import { json } from '@remix-run/node'; import { useLoaderData, useActionData, Form } from '@remix-run/react'; // type definitions // ... // loader function export const loader = async () => { // ... }; // action funtion export const action = async ({ request }) => { const formData = await request.formData(); const name = formData.get('name'); return json({ name }); }; export default function Index() { // use data from loader const { books } = useLoaderData() as LoaderData; // get data from action const data = useActionData(); return ( <div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.4' }}> {/* show "Stranger" if no data is available yet */} <h1>Welcome to Remix {data ? data.name : 'Stranger'} </h1> <ul> {books.map(({ title, genre }, i) => { return ( <li key={i}> <h3> {title} </h3> <p> {genre} </p> </li> ); })} </ul> {/* Remix form component with "POST" method */} <Form method="post"> <div className="form-control"> <label htmlFor="name"> Name <input id="name" name="name" type="text" /> </label> </div> <button type="submit">Submit </button> </Form> </div> ); }
In the code above, we created an action
function with a request
parameter. We obtain the form values by calling request.formData()
and passing it to the formData
variable. In order to get the name
value, we call the .get()
method on formData
.
Lastly, we return a JSON object containing the name
received from the request.
In our Index
component, in order to access the JSON parsed data from our route action
, we simply use the useActionData
hook. It returns undefined
if there hasn’t been a submission at the current location yet.
Well, that’s the most basic introduction to Remix and we’ve seen how we can get and send data in our Remix application. Next, we’ll take a look at GraphQL and how we can use it in Remix.
The major advantage that GraphQL has over a REST API is that GraphQL reduces the need to make more requests than is necessary when interacting with an API.
REST APIs, upon a request, tend to return more or sometimes less data than we need in our application. This might make the response from our request unnecessarily bloated, or even insufficient for an operation. We’ll then have to carry out another request, which, in turn, affects the user experience, especially in unstable network conditions).
With GraphQL, we have to ability to explicitly request what we need in a response — no more, no less.
Combining the efficiency of GraphQL with the efficiency Remix brings in building server-side rendered web applications, we’ll be looking at something truly awesome.
As defined in this post, “Apollo is a suite of tools to create a GraphQL server, and to consume a GraphQL API.“
One of the tools we’ll be using is Schema Link, which allows us to perform GraphQL operations on a provided schema instead of making a network call to a GraphQL API.
This tool, which we’ll be using alongside others as we move ahead in this article, comes in pretty handy for SSR applications.
Here’s a quick rundown of what we’re going to build in this tutorial.
In the previous section, we covered routes in Remix, loader
and action
functions, and to demonstrate the concepts, we built a simple app that renders a list of books and includes a form that asks and displays our name upon submission.
As a reminder, you can access the code on StackBlitz and on this GitHub branch.
In the following section, we’ll be building a simple app that displays a list of books and provides a form to upload new books in a nested route, all using GraphQL queries and mutations.
You can access the final code on the schema-links branch of the repository on GitHub.
To follow along with this article, we’ll need:
To create a new Remix project, run the following in the terminal:
npx create-remix@latest
Then follow the prompts:
? Where would you like to create your app? remix-graphql ? What type of app do you want to create? Just the basics ? Where do you want to deploy? Choose Remix App Server if you're unsure; it's easy to change deployment targets. Remix App Server ? TypeScript or JavaScript? TypeScript ? Do you want me to run `npm install`? Yes
Once the project has been created and installed, we can proceed.
In order to use Apollo GraphQL in our project, we have to install a few packages:
npm install @apollo/client @graphql-tools/schema
Once the packages are installed, let’s set up our GraphQL client. As aforementioned, we’ll be using Schema Link. In a new ./app/lib/apollo/index.ts
file, we’ll configure our schema and resolvers:
// ./app/lib/apollo/index.ts import { ApolloClient, gql, InMemoryCache } from "@apollo/client"; import { SchemaLink } from "@apollo/client/link/schema"; import { makeExecutableSchema } from "@graphql-tools/schema"; import { read, write } from "../../utils/readWrite"; // a schema is a collection of type definitions (hence "typeDefs") // that together define the "shape" of queries that are executed against // your data. export const typeDefs = gql` # Comments in GraphQL strings (such as this one) start with the hash (#) symbol. # This "Book" type defines the queryable fields for every book in our data source. type Book { title: String author: String } # the "Query" type is special: it lists all of the available queries that # clients can execute, along with the return type for each. in this # case, the "books" query returns an array of zero or more Books (defined above). type Query { books: [Book] } `; // resolvers define the technique for fetching the types defined in the // schema. this resolver retrieves books from the "books" array above. export const resolvers = { Query: { books: () => { const books = read(); return books; }, } }; const schema = makeExecutableSchema({ typeDefs, resolvers }); export const graphQLClient = new ApolloClient({ cache: new InMemoryCache(), ssrMode: true, link: new SchemaLink({ schema }), });
In the code above, we defined our Book
and Query
types. For our Query
, the books
query returns a list of Book
.
We also defined our Query
resolver in resolvers
, which simply returns a list of books provided by the read
function. This could be a function that fetches a list of books from an external API or database. In our case, we’ll simply be getting our books from a JSON file.
Then, we create an executable schema using makeExecutableSchema
and pass in the schema (typeDefs
) and resolvers
.
Finally, we define a new ApolloClient
instance as graphQLClient
and export it, ready to be used in our Remix loaders.
Before that, let’s set up our utility functions read
and write
to enable us to read from and modify the .json
file containing our list of books.
Create a JSON file at ./app/data/books.json
:
// ./app/data/books.json [ { "title": "The Awakening", "author": "Kate Chopin" }, { "title": "City of Glass", "author": "Paul Auster" }, { "title": "Harry Potter and the Deathly Hallows", "author": "JK Rowling" } ]
Create a new file ./app/utils/readWrite.ts
and enter the following code:
// ./app/utils/readWrite.ts import fs from "fs" // JSON file containing books array const dataPath = `${process.cwd()}/app/data/books.json`; // function to read file contents export const read = ( returnJSON = false, path = dataPath, encoding = "utf-8" ) => { try { let data = readFileSync(path, encoding); return returnJSON ? data : JSON.parse(data); } catch (error) { console.log({ error }); return null; } }; // function to write content to file export const write = (data: object, path = dataPath) => { let initialData = read(); let modifiedData = [...initialData, data]; try { writeFileSync(path, JSON.stringify(modifiedData, null, 2)); let result = read(); return result; } catch (error) { console.log({ error }); return null; } };
Great! Now that our read and write functions for our resolvers have been created, let’s create a /books
route to run a query that lists out all our books using the Apollo Client Schema links.
Create a new file ./app/routes/books.tsx
with the following code:
// ./app/routes/books.tsx import { LoaderFunction, json } from "@remix-run/node"; import { gql } from "@apollo/client"; import { graphQLClient } from "~/lib/apollo"; import { useLoaderData } from "@remix-run/react"; const query = gql` query GetBooks { books { title author } } `; export const loader: LoaderFunction = async ({ request, params }) => { const { data } = await graphQLClient.query({ query, }); return json({ books: data.books }); }; export default function Books() { const { books } = useLoaderData(); return ( <main> <section> <h1>All books</h1> <ul> {books.map(({ title, author }: { title: string; author: string }, index:number) => ( <li key={index}> <h3>{title}</h3> <p>{author}</p> </li> ))} </ul> </section> </main> ); }
Here, we define our query
and in our loader
function, we execute that query using graphQLClient.query()
and return the json
response.
In our Books
component, we get the books list using useLoaderData()
and render it:
Awesome! Our books/
route with the query works. Next, we’ll see how we can set up mutations.
First, we define our Mutation
and BookInput
types in typeDefs
in ./app/lib/apollo/index.ts
:
// ./app/lib/apollo/index.ts // ... export const typeDefs = gql` # here, we define an input input BookInput { title: String author: String } # here, we define our mutations type Mutation { addBook(book: BookInput): [Book]! } `;
Here, we defined an input
type: BookInput
with title
and author
. They’re both strings.
We also defined an addBook
mutation which accepts an argument, book
, of the input type we created earlier, BookInput
.
The addBook
mutation then returns a list of books called [Book]
Next, we define our Mutation
resolver:
// ./app/lib/apollo/index.ts // ... export const resolvers = { Query: { // ... }, Mutation: { addBook: (parent: any, { book }: any) => { console.log({ book }); let books = write(book); return books; }, }, }; // ...
Here, we create a new addBook
resolver that takes in book
as an argument and passes it to the write()
function. This adds the new book to the list and returns the updated list of books.
Create a new /books/addbook
route. This is going to be a nested route, which means that we’ll have to create a books
directory like ./app/routes/books/addbook.tsx
:
// ./app/routes/books/addbook.tsx import { gql } from "@apollo/client"; import { ActionFunction, json } from "@remix-run/node"; import { Form } from "@remix-run/react"; import { graphQLClient } from "~/lib/apollo"; // action function export const action: ActionFunction = async ({ request }) => { const formData = await request.formData(); const title = formData.get("title"); const author = formData.get("author"); let book = { title, author, }; // mutation to add book const mutation = gql` mutation ($book: BookInput) { addBook(book: $book) { title } } `; const { data } = await graphQLClient.mutate({ mutation, variables: { book }, }); return json({ books: data.books }); }; export default function AddBook() { return ( <section style={{ border: "1px solid #333", padding: "1rem" }}> <h2>Add new book</h2> <Form method="post"> <div className="form-control"> <label htmlFor="title">Title</label> <input id="title" name="title" type="text" /> </div> <div className="form-control"> <label htmlFor="author">Author</label> <input id="author" name="author" type="text" /> </div> <button type="submit">Submit</button> </Form> </section> ); }
Here, we can see that we have an action
function where we get the title
and author
from the request
.
Then, we create a mutation
and pass the book
object containing the title
and author
as a variable. After that, we execute this query using graphQLClient.mutate
.
In our AddBook
component, we use the Form
component provided by Remix to send our data with method = "post"
.
Now, in order to render our nested route, we have to add <Outlet/>
in ./app/routes/books.tsx
:
// ... export default function Books() { const { books } = useLoaderData(); return ( <main> <section> {/* ... */} </section> <Outlet /> </main> ); }
Now, when we go to http://localhost:3000/books/addbook
we should see this:
Nice!
So far, we’ve been able to set Remix with Apollo GraphQL to make requests against our GraphQL schema.
We can now make queries and mutations and it all happens server-side, so there are no unnecessary network calls on our client.
This is just the tip of the iceberg of what we can achieve with Remix and GraphQL. We can also create a resource route in Remix that we can use to provide a GraphQL endpoint using Apollo Server. This will allow us to make requests against and also provide a GraphQL playground.
You can read more about resource routes in the Remix docs and also about Apollo Server in the Apollo documentation.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.