There are 2 approaches to defining your schema in GraphQL: schema-first or code-first.
In schema-first, you write .graphql
(or .gql
) files, while in code-first you write resolver-like types in Javascript (or Typescript).
Now, if you're using Typescript, you might find yourself having to write types again for other purposed, your resolvers for example. That can quickly become a problem, not only because it feels like a waste of time, but also because it makes it that much harder to maintain. If your codebase and your schema grow in complexity, and you have a whole team working on it, a small type definition update can cause a huge mess!
If we look at a fullstack Typescript application, we have to duplicate our type definition at least 3 times:
- in the schema file
- in the backend resolvers
- in the frontend for the GraphQL client
- BONUS: for the ORM
Enters GraphQL code generator
GraphQL code generator is the solution to that problem: you write your schema file and the rest gets generated automatically!
Now let's see how it actually works 👇
This was originally posted on blog.escape.tech
Example: Building a (over-engineered) blog
I know you love using overly complicated tools to build your blog so let's just do that cause why not 🤷♂️
Here are the graph relations:
As mentioned above, with this stack we would normally have to:
- write our type definitions in a schema file
- write types for our backend resolvers
- write model definitions for our ORM (using Prisma in this example)
- write types for React Query on the frontend
Phew, that's a lot of effort!
Now imagine if in 4 months we decide to add tags to our posts. We would have to go through the same 4 steps to update the types!
But with GraphQL codegen, we have one single source of truth: the schema file!
Alright, enough teasing, let's jump into the code!
Backend with Express and Express GraphQL
If you start from scratch, you can simply install Express, Express GraphQL and Typescript (+ some other utilities):
npm install express express-graphql @graphql-tools/schema cors import-graphql-node
npm install -D @types/express
Then we can very easily setup the server:
import "import-graphql-node"
import express from "express"
import {GraphQLHTTP} from "express-graphql"
import cors from "cors"
import {makeExecutableSchema} from "@graphql-tools/schema"
import * as typeDefs from "./schema.graphql"
const app = express()
app.use(cors())
const schema = makeExecutableSchema({ typeDefs })
app.use("/", GraphQLHTTP({
context: {db},
schema: schema,
graphql: true
}))
Note here that I'm using import-graphql-node
to import .graphql
files.
Checkout the repo for more details.
Frontend with React and React Query
We can bootstrap a React and Typescript project very easily with the Create React App boilerplate:
npx create-react-app client --template typescript
Next, let's add React Query:
npm install react-query
and set it up:
import "./style.css"
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"
import {QueryClient, QueryClientProvider} from "react-query"
const client = new QueryClient()
ReactDOM.render(
<React.StrictMode>
<QueryClientProvider client={client}>
<App />
</QueryClientProvider>
</React.StrictMode>,
document.getElementById("root")
)
Setting up GraphQL codegen
Setting up GraphQL codegen is super easy! First, install the CLI:
npm install -D @graphql-codegen/cli
Then launch the initialisation proces:
npx graphql-codegen init
This will prompt a series of questions to set it up for your needs. It's not super important as it's very easy to update the configuration later.
Here is (approximately) the config file that you'll end up with:
Let's go over each field to explain what it does and configure them exactly how we need it.
Schema
This should point to your schema definition. By default, it uses your GraphQL endpoint, but in general it's easier to put the path to your actual schema file:
schema: "server/schema.graphql"
Documents
This is part of the frontend configuration. Documents should point to some schema definition of your operations (queries and mutations). Here's an example:
query AllPosts {
allPosts {
id
author {
displayName
picture
}
title
publishedAt
content
comments {
id
text
username
}
}
}
documents: "client/**/*.graphql"
The React Query Plugin
The installation process didn't gave us an option to add React Query. But we can easily integrate it thanks to the huge pugin hub!:
Fist, we need to install the right plugin:
npm install -D @graphql-codegen/typescript-react-query
Then we configure it in codegen.yml
configuration file by adding it to the plugins of the frontend section:
generates:
client/src/generated.tsx:
documents: "client/**/*.graphql" # where the queries are written
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-query"
What's amazing about this plugin is that it's also going to take care of configuring the React Query client (endpoint, fetcher, etc) so that we can just use simple hooks, eg. useGetAllPostsQuery()
In order to make this work, we need to provide some configuration such as the GraphQL endpoint, but we can also add other things, e.g, an authorization header (with environment variables, how cool is that!):
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-query"
config:
fetcher:
endpoint: "process.env.API_URI"
fetchParams:
headers:
Content-Type: "application/json"
Authorization: "Bearer process.env.HEADER_AUTH_TOKEN"
Putting everything together
We are now ready to go!
To generate the types, we can simply run the command:
npm run codegen
Use the generated types in the backend resolvers:
import type {QueryAuthorArgs} from "/generated"
import type {Context} from "./context"
const resolvers = {
Query: {
author: (
_parent: null,
{ id }: QueryAuthorArgs,
context: Context) => {
// Do what you got to do to get the author...
}
}
Mutation: {
createPost: (
_parent: null,
{ input }: MutationCreatePostArgs,
ctx: Context
) => {
// Save the post in the database!
},
}
}
And use the generated hooks in the frontend like so:
import { useAllPostsQuery } from "./generated";
function App() {
const { status, error, data } = useAllPostsQuery();
...
Conclusion
If you decide to go down the code-first route (blue pill) good for you, but many teams decide to pick a schema-first approach to build their GraphQL API, and even though it's a great option, it can quickly become a burden to test and maintain your code.
But fortunately, graphql-codegen is an elegant solution to fix that problem of code duplication, making the schema file your single source of truth!
GraphQL Security
In one of our previous post, we shared how every GraphQL framework has zero security configured by default. Most GraphQL APIs are therefore subject to the most basic attacks (brute force, DoS, etc).
To compensate this lack of security in the GraphQL ecosystem, we built a quick scan that will get you started im your journey to shipping bullet proof applications!
You can run a dozen security scans on your GraphQL endpoint for free - no signup - at graphql.security
Top comments (1)
See: The Problems of "Schema-First" GraphQL Server Development
The red pill path is:
Back-end: Code-first (aka. resolver-first) using Nexus GraphQL, which outputs both GraphQL schema and TS types and keeps them in sync. Possibly with the Prisma (ORM) plugin to quickly expose DB fields without having to write resolvers.
Front-end: GQty (runtime gen). Alternatively GraphQL codegen (compiletime gen) to generate React useNamedQuery hooks with TS typings from the GraphQL schema (which is schema-introspected from the back-end). Possibly with graphql-codegen plugin called typescript-urql if using URQL GraphQL client (which is recommended).