Rails, GraphQL, and TypeScript: a stack worth trying
Tags: rails, graphql, typescript, api
Published: 2020-04-13 00:05:37

---
title: Rails, GraphQL, and TypeScript: a stack worth trying
published: true
date: 2020-04-12 07:00:00 UTC
tags: ["rails", "graphql", "typescript", "api"]
canonical_url: https://stevenpetryk.com/blog/rails-graphql-typescript-stack/
---

Ruby on Rails has been a powerful framework for building web apps for well over a decade. In recent years, Rails has expanded its capabilities to support modern frontend technologies with barely any configuration. In doing some proof-of-concept work for an app idea, I built a stack I'm quite happy with.

- Rails serves a GraphQL API.
- Rails _also_ serves a webpack-based frontend, and compiles it for us.
- Types are **synchronized between GraphQL and TypeScript**, so I can never misuse query results.

All of this was achieved with very little configuration thanks to Rails' excellent defaults.

If you'd like to see a working example, I've built a [proof-of-concept repository][repo] with these features fully set up. The commits mimic the steps in this post.

---

## Starting with Rails

I began with a fresh Rails app, but it's just as easy to set this up with an existing one, especially if you're already using GraphQL and/or Webpacker.

If you're just starting out with Rails, check out the official [getting started guide][rails-getting-started]. This walkthrough assumes you have a little familiarity with Rails.

[rails-getting-started]: https://guides.rubyonrails.org/getting_started.html

## Set up GraphQL

The `graphql` gem makes setting up our GraphQL API as easy as running a few commands. First, add the gem to your `Gemfile`, somewhere above all the `group` declarations:

```rb
#...
gem 'bootsnap', '>= 1.4.2', require: false

gem "graphql"

group :development do
  # ...
```

Then, update your dependencies and generate the GraphQL Ruby boilerplate:

```bash
bundle install
rails generate graphql:install
```

This generator attempts to give you a nice [GraphiQL][graphiql] UI to query your API in development, but it didn't work for me out of the box. I had to add the following lines to `app/assets/config/manifest.js`. You may have to, too:

```javascript
//= link_tree ../images
//= link_directory ../stylesheets.css
//= link graphiql/rails/application.js
//= link graphiql/rails/application.css
```

Restart your server. If you go to [http://localhost:3000/graphiql](http://localhost:3000/graphiql), you'll see a [GraphiQL console][graphiql] in your browser so you can query your new GraphQL API. Run a query, just to make sure everything's wired up properly.

![](https://stevenpetryk.com/images/uploads/graphiql-fetch.png)

[graphiql]: https://github.com/graphql/graphiql

## Setting up the frontend

Rails uses webpack to compile assets by default. If you're on Rails 6, it's likely already set up in your application. To set up TypeScript support, you can use a simple command provided by Webpacker:

```bash
bundle exec rails webpacker:install:typescript
```

This will add some configuration to Webpacker to enable TypeScript support. Let's also convert Rails' generated JavaScript file to TypeScript and delete the example file the generator produced:

```bash
mv app/javascript/packs/application.js app/javascript/packs/application.ts
rm app/javascript/packs/hello_typescript.ts
```

Next, let's set up a blank page just so we have a place to run our JavaScript. If you already have such a route, skip this part. For brevity, we'll use `ApplicationController`, but in production you'd be best off making a controller specifically to serve your frontend. Any actions placed in `ApplicationController` will be inherited by all controllers in your application, which could lead to chaos.

In `application_controller.rb`, create the empty controller action:

```rb
class ApplicationController < ActionController::Base
  def index
  end
end
```

Create a (mostly) empty view:

```bash
mkdir app/views/application
echo "<h1>Hello, world</h1>" > app/views/application/index.html.erb
```

Then define a route that sends requests to `/` to this route in `config/routes.rb`

```rb
Rails.application.routes.draw do
  if Rails.env.development?
    mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
  end
  post "/graphql", to: "graphql#execute"

  root to: "application#index"
end
```

Here you can also spot how, earlier, the `graphql` generator added a GraphQL endpoint and the GraphiQL page.

Finally, let's add some code to `application.ts` to verify things are working. Rails also starts a few of its built-in library in this file—I'm going to leave those out from now on.

```typescript
// <some require() calls for tubolinks, activestorage, etc.>

console.log("Hello from the console!")
```

Refresh http://localhost:3000. You should see "Hello, world!" on the page and another, similarly zealous greeting in the console.

You might notice that loading the page took a while. This is because Webpacker is recompiling your whole webpack app on every request. You can instead tell Webpacker to run a separate development server that will recompile only when you change files. In a separate terminal, run this:

```bash
bin/webpack-dev-server
```

If you reload the page, you'll see it's super quick now. Rails automatically defers asset requests to the webpack development server rather than triggering a webpack compilation on every request.

## Making GraphQL calls from our frontend

Let's have our component actually run a GraphQL request against our API. First, install the Apollo client:

```bash
yarn add @apollo/client react
```

This comes with TypeScript typings for free. In fact, everything in Apollo's ecosystem is written in TypeScript. You may have noticed we also added `react`—we won't actually be using React in this tutorial, but the Apollo client is a React client. If this doesn't work for you, you're welcome to use any one of Apollo's integrations with other frameworks.

Let's set up the client in `application.ts`:

```typescript
import { ApolloClient, HttpLink, InMemoryCache, gql } from "@apollo/client"

// <some require() calls for tubolinks, activestorage, etc.>

const railsCsrfToken = document
  .querySelector("meta[name=csrf-token]")
  ?.getAttribute("content")

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: new HttpLink({
    uri: "http://localhost:3000/graphql",
    headers: {
      "X-CSRF-Token": railsCsrfToken,
    },
  }),
})
```

We're extracting Rails' CSRF token from the page and sending it along with the GraphQL requests—it's one cool advantage of serving our frontend from Rails. An alternative strategy is to disable CSRF protection within `GraphqlController`.

Then, below that, we'll tell the client to make a GraphQL request:

```typescript
const GET_TEST = gql`
  query Test {
    testField
  }
`

client
  .query({ query: GET_TEST })
  .then(result => console.log("GraphQL result: ", result.data))
```

If you refresh the page, you should see the GraphQL API call result in the console.

![](https://stevenpetryk.com/images/uploads/client-results.png)

## Two type systems: GraphQL and TypeScript

This section is the crux of this guide. Look in `application.ts`—TypeScript has no idea what `result.data` looks like. It could be an object, array, string, even a function—its type is `any`, and `any` is dangerous in this context.

<div style="text-align: center">
  <img src="https://stevenpetryk.com/images/uploads/type-any.gif" style="max-width: 764px; width: 100%; border-radius: 5px" alt="" />
</div>

Imagine you're querying a user, but forget to ask for their name. Wouldn't you like to know that `result.data.name` will never exist? If we had more complete types, such a mistake would be stopped at build time.

```typescript
const GET_CURRENT_USER = gql`
  query GetCurrentUser {
    currentUser {
      # forgot to ask for "name"
      birthday
    }
  }
`

client
  //     ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ if we had types like this...
  .query<GetCurrentUserType>({ query: GET_CURRENT_USER })
  .then(result => {
    // ... then this wouldn't compile (a good thing).
    alert("Hello, " + result.data.currentUser.name)
  })
```

We need a way to know the shape of this response. We could generate it manually, but that's tiresome, and feels silly when we have a GraphQL schema with sound typing.

### Apollo codegen

Enter [`apollo client:codegen`][apollo-tooling]. It's a code generator that can take a GraphQL schema, scan a repository for GraphQL queries, and turn them into type declarations. It supports TypeScript, Flow, and Swift.

[apollo-tooling]: https://github.com/apollographql/apollo-tooling#apollo-clientcodegen-output

### Export the schema to SDL

First, we need our schema in a format Apollo understands. We can't just pass it our Ruby file—Apollo doesn't integrate at all with the GraphQL gem. Instead, we can tell GraphQL Ruby to output our schema in the GraphQL SDL (Schema Definition Language), which Apollo _does_ understand. From there, Apollo can connect our schema to our queries and generate typings for each query.

The first step is getting GraphQL Ruby to give us the schema. Create `lib/tasks/graphql.rake` and define this task inside of it:

```rb
task dump_graphql_schema: :environment do
  # Replace "MySchema" with your schema's Ruby class name.
  schema_definition = MySchema.to_definition
  schema_path = Rails.root.join("app", "graphql", "schema.graphql")
  File.write(schema_path, schema_definition)
  puts "Updated #{schema_path}"
end
```

GraphQL Ruby's `Schema#to_definition` method returns a string containing exactly what we need—our schema in SDL. Let's run this new rake task:

```bash
rails dump_graphql_schema
```

This creates `app/graphql/schema.graphql`, our schema in pure GraphQL SDL.

### Set up the Apollo CLI

Now that this exists, we can easily tell the Apollo CLI how to generate types. Let's first install it:

```bash
yarn add --dev apollo
```

Then we'll make a new file at the root of our directory, `apollo.config.js`, and tell it how to find everything we've built and where to put types. Again, replace `MySchema` with your schema name.

```js
module.exports = {
  client: {
    output: "app/javascript",
    outputFlat: true,
    includes: ["./app/javascript/**/*.ts"],
    tagName: "gql",
    service: {
      name: "MySchema",
      localSchemaFile: "./app/graphql/schema.graphql",
    },
  },
}
```

From top to bottom, we have:

- `output`: where to put types
- `outputFlat`: stick all generated types into one file
- `includes`: where to find all the queries we're writing
- `tagName`: the name of the function we wrap our queries in
- `service.name`: the name of the schema
- `service.localSchemaFile`: the location of the file we generated in the previous step

Lastly, we'll set up an NPM script to run our Rake task and the Apollo codegen. In `package.json`, add the following:

```json
{
  "name": "cms",
  "private": true,
  "scripts": {
    "graphql:codegen": "bin/rails dump_graphql_schema && apollo client:codegen --target typescript"
  }
  // ...
}
```

Let's run it:

```bash
yarn graphql:codegen
```

And now, with very little work, you've got types inside of `app/javascript/packs/__generated__` for each of your queries. The name of each file (and interface inside each file) is the same as the name of your particular GraphQL query.

You can run this command whenever you make changes to your schema, or use something like [Guard](Ruby) or [Watchman](Node) to run it whenever relevant files change. I used Guard to run this command whenever `app/graphql/**/*.rb` and/or `app/javascript/**/*.ts` changes.

You should also run this command in CI whenever you compile your TypeScript—it'll ensure your types are up-to-date before checking them.

[guard]: https://github.com/guard/guard
[watchman]: https://facebook.github.io/watchman/

---

## Tying it all together

At last, we have all the pieces we need to add types to our GraphQL query. Open up `application.ts` again, and add the types that were generated.

```typescript
import { ApolloClient, HttpLink, InMemoryCache, gql } from "@apollo/client"
import { Test } from "./__generated__/Test"

// <rails require() calls, yet again>

const railsCsrfToken = document
  .querySelector("meta[name=csrf-token]")
  ?.getAttribute("content")

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: new HttpLink({
    uri: "http://localhost:3000/graphql",
    headers: {
      "X-CSRF-Token": railsCsrfToken,
    },
  }),
})

const GET_TEST = gql`
  query Test {
    testField
  }
`

client
  .query({ query: GET_TEST })
  .query<Test>({ query: GET_TEST })
  .then(result => console.log(result.data))
```

Merely the fact that we wrote a query named `Test` caused its return type to be generated. This is great for us, because it means we can ensure that `result.data` is exactly the shape it will actually be in production.

<div style="text-align: center">
  <img src="https://stevenpetryk.com/images/uploads/now-there-are-types.gif" style="max-width: 764px; width: 100%; border-radius: 5px;" alt="" />
</div>

---

## Closing thoughts

- I've made an [example repository][repo] with this setup. Feel free to clone, alter, open issues, turn it into a generator—whatever!

- The natural continuation of this is to start building React components. For that, you'll have to [set up Webpacker to support React][webpacker-react]. You may have to alter `tsconfig.json` after running the React generator.

- You should also add these generated files (both the TypeScript files and the GraphQL schema) to your `.gitignore`.

- Since we made an `apollo.config.js`, Apollo's [Visual Studio Code extension][apollo-extension] will work out of the box and annotate your query's types.

Hope that was fun! If you have any questions or feedback, or just want to know when I write something else, [follow me on Twitter][twitter].

[rails]: https://rubyonrails.org/
[repo]: https://github.com/stevenpetryk/graphql-typescript-example
[graphql-ruby]: https://graphql-ruby.org/
[webpacker]: https://github.com/rails/webpacker
[webpacker-react]: https://github.com/rails/webpacker/blob/master/docs/integrations.md#react
[apollo]: https://www.apollographql.com/
[twitter]: https://twitter.com/stevenpetryk
[apollo-extension]: https://marketplace.visualstudio.com/items?itemName=apollographql.vscode-apollo