DEV Community

Cover image for Improving API Documentation using React Query and TypeScript
Arnav Gosain
Arnav Gosain

Posted on • Updated on • Originally published at


Improving API Documentation using React Query and TypeScript

As your codebase grows, there is only one way to increase developer productivity: Documentation. One of many reasons I prefer TypeScript to JavaScript is that overtime, as your codebase grows, developer productivity increases because TypeScript (and typed languages in general) offer something that dynamically typed languages cannot, automatic documentation in your IDE.

This article assumes you're familiar with React Query. If you're not, I highly recommend you read the official docs and this intro guide by Sai Kranthi.

Why React Query

Imagine an simple app that does two things based on the PokeAPI:

  1. Renders a list of Pokemons that link to their own dedicated page
  2. Have dedicated pages for all pokemons

To fetch the list of pokemons, with Redux (before RTK Query) you would have to:

  1. Create a global store
  2. Create a reducer with an action to update the list in the store
  3. Write a thunk action to fetch the data.
  4. Write a useEffect hook inside to dispatch the thunk action.
  5. Render the list.

And then you'd have to write invalidation logic, loading status logic and much more.

But with React Query, fetching your list of pokemons is as easy as wrapping your App in a QueryClientProvider and then making use of the useQuery and useMutation hooks.

Example of basic React Query usage:

This approach works for simple apps like a Pokemon List, but it quickly becomes unmanageable as you add more endpoints to your API. In which case, you would have to create many such custom hooks.

This is the problem I ran into as I hopped on my first project after joining TartanHQ. While it's a fairly simple CRUD app, it makes use of many endpoints and making custom hooks for each endpoint simply isn't an option.

One Hook For All Queries

To counteract this problem, we created a layer of abstraction over React Query's useQuery hook, a hook that makes use of TypeScript to improve discoverability of endpoints across the entire application.

import * as React from "react";
import {
  useQuery as useReactQuery,
} from "react-query";
import { queryFetchers, QueryKeys } from "~/lib/api/queries";

type Await<T>  = T extends Promise<infer U> ? U : T;

export function useQuery<
  Key extends QueryKeys,
  Params = Parameters<typeof queryFetchers[Key]>,
  Data = Await<ReturnType<typeof queryFetchers[Key]>>
>(key: Key, options?: UseQueryOptions<Data>): UseQueryResult<Data>;

export function useQuery<
  Key extends QueryKeys,
  Params = Parameters<typeof queryFetchers[Key]>,
  Data = Await<ReturnType<typeof queryFetchers[Key]>>
  key: Key,
  params: Params,
  options?: UseQueryOptions<Data>
): UseQueryResult<Data>;

export function useQuery<
  Key extends QueryKeys,
  Params = Parameters<typeof queryFetchers[Key]>,
  Data = Await<ReturnType<typeof queryFetchers[Key]>>
  key: Key,
  arg2?: Params | UseQueryOptions<Data>,
  arg3?: UseQueryOptions<Data, unknown, Data>
) {
  const params = Array.isArray(arg2) ? arg2 : [];
  const options = !!arg3 && Array.isArray(arg2) ? arg3 : arg2;

  return useReactQuery(
    () => queryFetchers[key].apply(null, params),
Enter fullscreen mode Exit fullscreen mode
 * Legend:
 * QKEY = Query Key
 * QData = Query Data

const GET_ALL_POKEMONS_QKEY = "pokemons/all" as const;
type GetAllPokemonsQData = {
  count: number;
  next: string;
  previous: string;
  results: { name: string; url: string }[];
const getAllPokemons = (): Promise<GetAllPokemonsQData> => {
  return fetch("").then(
    (response) => response.json() as GetAllPokemonsQData

const POKEMON_BY_ID_QKEY = "pokemons/byId" as const;
type GetPokemonByIdQData = Record<string, unknown>;
const getPokemonById = (id: string) => {
  return fetch(`${id}/`).then(
    (res) => res.json() as GetPokemonByIdQData

export type QueryKeys = typeof GET_ALL_POKEMONS_KEY | typeof POKEMON_BY_ID_QKEY;
export const queryFetchers = {
  [GET_ALL_POKEMONS_QKEY]: getAllPokemons,
  [POKEMON_BY_ID_QKEY]: getPokemonById,
} as const;
Enter fullscreen mode Exit fullscreen mode


Now that you're all done, you can take full advantage of VSCode autocomplete.

Custom useQuery in Action

If you have an alternative idea or found this useful: I'd love to connect with you on Twitter!

Top comments (2)

phryneas profile image
Lenz Weber • Edited

To be fair, the above does describe a valid way of doing this with Redux - but the official Redux Toolkit also contains "RTK Query", which does essentially the same as React Query. In RTK Query, a similar functionality would look like

// Need to use the React-specific entry point to import createApi
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { Pokemon } from './types'

// Define a service using a base URL and expected endpoints
export const pokemonApi = createApi({
  reducerPath: 'pokemonApi',
  baseQuery: fetchBaseQuery({ baseUrl: '' }),
  endpoints: (builder) => ({
    getPokemonById: builder.query<Pokemon, string>({
      query: (id) => `pokemon/${id}`,
    getAllPokemon:  builder.query<Record<string,Pokemon>, void>({
      query: (id) => `pokemons/all`,

// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const { useGetPokemonByIdQuery, useGetAllPokemonQuery } = pokemonApi
Enter fullscreen mode Exit fullscreen mode

And this either integrates in your Redux store or you don't set a Redux store up, but just wrap your app into a <ApiProvider api={pokemonApi}> which will automatically set up a Redux store for you.

If you're interested in more, check out

If you want to play around with it, here is a CodeSandbox:

arn4v profile image
Arnav Gosain

I haven't had a chance to try RTK Query, will try it out!

Not mentioning RTK Query is an oversight on my end, I have edited the post to mention it now.

11 Tips That Make You a Better Typescript Programmer


1 Think in {Set}

Type is an everyday concept to programmers, but it’s surprisingly difficult to define it succinctly. I find it helpful to use Set as a conceptual model instead.

#2 Understand declared type and narrowed type

One extremely powerful typescript feature is automatic type narrowing based on control flow. This means a variable has two types associated with it at any specific point of code location: a declaration type and a narrowed type.

#3 Use discriminated union instead of optional fields


Read the whole post now!