DEV Community

loading...
Cover image for Fully typed API responses using GraphQL

Fully typed API responses using GraphQL

santosh898 profile image santosh898 ・5 min read

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
  }
}
`
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.🎊

image

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"],
  },
};
Enter fullscreen mode Exit fullscreen mode

after adding this config, the extension downloads the schema from the URL and provides the IntelliSense when you write the queries.

image

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)

image

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.

Discussion (1)

pic
Editor guide
Collapse
patarapolw profile image
Pacharapol Withayasakpunt

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.