At my workplace, we were setting up the environment for a brand-new app and decided to go with typescript to get the most out of the static analysis it provides. We have a standard architecture, a REST API, and react/react-native frontend.
Furthermore, we were able to make sure that the whole app is type-safe except for the part where we consume the API responses. I had to manually write the types for the API responses. The biggest problem with this approach is there is no way we can ensure the responses are type-safe because we are just "assuming" their types.
Then I wondered what would happen if we have GraphQL in the picture and set out on a journey. If you don't know what GraphQL is, it's a Query language for your API, where you define what your API can provide as strictly typed schema and clients will consume a subset of it.
I decided to build a POC using GraphQL with a complete type-safe frontend. You can access the full code(server and client) here.
Server-side
I won't be dealing with the server-side aspect of this, and I won't go deep into GraphQL as well. The following is the schema of my API.
the server is written using node+typescript+apollo-sever
Schema
type Book {
id: Float!
title: String!
subtitle: String!
author: String!
published: String
publisher: String
pages: Int!
description: String!
website: String!
}
type Query {
getBooks(limit: Int): [Book]
getBookDetails(id: Float): Book
}
In the above schema, type Book
is a resource, and type Query
is where we define what kind of queries are supported.getBooks
will respond with an array of Books
and getBookDetails
will respond with a Book
for the given ID
.
Client-side
So we have the following problems to crack.
- Connect front-end to GraphQL.
- Get Fully typed responses automatically.
- IntelliSense when writing the queries.
Connect the front-end to GraphQL
I created a GraphQL powered react-app using Create Apollo app by running
yarn create apollo-app client
It has out-of-the-box support for .graphql
files and the boilerplate code to connect to the backend.
See client/src/index.tsx
But later I found that the template is pretty old and upgraded graphql
and migrated from react-apollo
to @apollo-client
.
I used the create-apollo-app for generating the server codebase as well.
We can consume data by using the useQuery
hook from @apollo/client
like this.
client/src/queries/getBooks.ts
import { gql } from "@apollo/client";
export const GET_BOOKS = gql`
query getBooks($limit: Int) {
getBooks(limit: $limit) {
id
title
subtitle
author
}
}
`
client/src/ListBooks.tsx
import { useQuery } from "@apollo/client";
import { GET_BOOKS } from './queries/getBooks.ts'
const ListBooks: React.FC<{}> = () => {
const { loading, error, data } = useQuery(GET_BOOKS,{
variables: {
limit: 5,
},
});
...render data
}
It works but the data isn't fully typed yet.
Get Strongly typed responses automatically
To avoid writing the types for the responses manually we are going to use GraphQL Code Generator.
graphql-codegen is a CLI tool that generates types automatically from the provided GraphQL Schema. They have a lot of plugins and options for generating the types for both frontend and backend.
By using this we can have the server-side written GraphQL schema as a single source of truth for the whole application.
The setup is pretty straightforward. Refer to the Installation page:
# install the cli as a dev-dependency
yarn add -D @graphql-codegen/cli
# Step by step walkthrough initialization
yarn graphql-codegen init
The above code adds relevant dev-dependencies based on our selection and creates a codegen.yml
file at the project root. My codegen.yml
file looks like this.
overwrite: true
schema: "http://localhost:8080/graphql"
documents: "src/**/*.ts"
generates:
src/queries/typedQueries.ts:
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-apollo"
I'll walk you through the options.
- schema - the URL for the schema. Can be a file, function, string as well. See the documentation.
-
documents - where to search for the GraphQL queries and fragments. I asked it to search in the
.ts
files - generates - the target file path.
-
plugins - automatically added based on the options selected in the
init
- typescript - bare minimum plugin to generate types.
- typescript-operations - to generate types for the GraphQL Operations
- typescript-react-apollo - to generate typed hooks for the queries written and other support for the @apollo/client.
Now after running yarn generate
it'll generate the file typedQueries.ts
. And I updated my component to use the generated hook
.
import React from "react";
import { useGetBooksQuery } from "./queries/typedQueries.ts";
const ListBooks: React.FC<{}> = () => {
const { loading, error, data } = useGetBooksQuery({
variables: {
limit: 5,
},
});
...render data
}
What changed here? we are not importing the query anymore, the generated types will do that for us. and guess what? the data
is fully typed.🎊
Instead of having to run yarn generate
every time we change a query, we can run the codegen-cli in watch mode as well.See Documentation.
Note: If you are going to use node for the backend, codegen-cli will be helpful for generating types from Schema so that you don't need to define the types two times. Refer to server code in my codebase for reference.
IntelliSense when writing the queries (in vscode)
The Apollo VS Code extension provides an all-in-one tooling experience for developing apps with Apollo.
We will get the syntax highlighting by just installing the extension. But to have IntelliSense we have to add a config file to the project.
client/apollo-config.js
module.exports = {
client: {
service: {
name: "my-graphql-app",
url: "http://localhost:8080/graphql",
},
excludes: ["**/src/queries/typedQueries.ts"],
},
};
after adding this config, the extension downloads the schema from the URL and provides the IntelliSense when you write the queries.
One last thing! when I updated the schema, the extension didn't pick that up, so I had to run reload schema
manually. (ctrl+shift+p
to open command palette)
I don't know if there's an option for this to happen automatically. I didn't go deep into the extension documentation.
That's it. Now I have a strong type system for API calls plus added benefits of GraphQL.
This is my first ever post. If you are still here, thank you for having the patience to stay this long.
Top comments (1)
Might be more handy than trying to do this in REST API's OpenAPI / Swagger.
Yes, this is possible with some codegen as well, e.g. OpenAPI-client-axios, swaxios.