API

Learn how to make API requests using Axios and Tanstack Query.

We use Tanstack Query and Axios to make API requests. Tanstack Query is a powerful async data fetching and caching library. Axios is a simple promise-based HTTP client for the browser and Node.js.

Axios Usage

For making API requests, it's advantageous to use a pre-configured, reusable Axios instance throughout the application. This has already been set up for you in the src/lib/axios.ts file.

The configuration includes an interceptor that automatically displays a toast message when an error occurs.

For every API call, ensure you import the Axios instance from src/lib/axios.ts instead of the default Axios instance from "axios".

import { axios } from "@/lib/axios"

Tanstack Query Usage

To make data fetching easier and more reusable, I recommend creating custom hooks using Tanstack Query.

Instead of writing API requests on the fly, define and export them separately. This keeps your code clean and organized, with everything in one place.

For each API request, you should:

  1. Define types for the request and response data.
  2. Create a fetcher function that calls the endpoint using the pre-configured Axios API client instance.
  3. Build a hook that uses this fetcher function.

This approach helps you keep track of all your endpoints and improves type safety by defining the response types clearly.

Here's an example of a custom hook that fetches a user profile:

import { useQuery } from "@tanstack/react-query"

import type { UserProfile } from "../types"

import { axios } from "@/lib/axios"
import { QUERY_KEYS } from "@/utils/queryKeys"

async function getUser(userId: string) {
  const { data } = await axios.get<UserProfile>(`/api/admin/users/${userId}`)
  return data
}

export function useUserProfile(userId: string) {
  return useQuery({
    queryKey: QUERY_KEYS.USER_PROFILE(userId),
    queryFn: () => getUser(userId),
  })
}

You can use this hook in your component to fetch user profiles like so:

export function UserProfile({ userId }: UserProfileProps) {
  const userProfileQuery = useUserProfile(userId)

  if (userProfileQuery.isPending) {
    return <Spinner />
  }

  return <UserProfileContent user={userProfileQuery.data} />
}

Query Keys

Query keys uniquely identify queries in the cache, allowing you to reference, invalidate, or refetch the data. They are usually strings or arrays of strings.

To keep everything organized, I recommend defining query keys in a separate file. I've set up a src/utils/queryKeys.ts file for you.

This file exports a QUERY_KEYS object, which provides a structured way to generate query keys for React Query.

  • Each property of the QUERY_KEYS object is a function returning an array of strings. This array is used as the query key in React Query.
  • Some functions take parameters to create more specific query keys.
  • It also includes a helper merge function to combine the base key with additional strings or arrays of strings, creating unique keys for each query.

Here is how the USER_PROFILE query key is defined in the src/utils/queryKeys.ts file.

export const QUERY_KEYS = {
  STORE: ["STORE"],
  USER_PROFILE: (userId: string) =>
    merge(QUERY_KEYS.STORE, "USER_PROFILE", userId),

  ...your other query keys
}

You can read more about query keys in the React Query documentation.

Sending Errors in API Routes

Creating API routes with Next.js is straightforward. You can define API routes in the src/app/api directory.

To handle errors consistently, I have created a helper function called sendError in the packages/utils/functions/error.ts file.

The sendError function takes three parameters:

  • error: The error object.
  • message: The error message to send in the response.
  • status: The HTTP status code to return.

Here's an example of how to use the sendError function in an API route:

import { withAuth } from "@package/auth"
import { sendError } from "@package/utils"
import { NextResponse } from "next/server"

export const GET = withAuth(({ user }) => {
  try {
    return NextResponse.json(user, { status: 200 })
  } catch (error) {
    return sendError(error, "Failed to get profile.", 500)
  }
})

Ignore the withAuth higher order function for now in the example above. It checks if the user is authenticated before executing the API route. I will cover authorization in the next section.

Custom Error Class

In addition to the sendError function, I have also created a custom error class called CustomError in the packages/utils/functions/error.ts file.

The CustomError class allows you to create custom error messages with specific status codes. You can use this class to throw errors in your API routes.

Here's an example of how to use the CustomError class in an API route:

import { withAuth } from "@package/auth"
import { CustomError, sendError } from "@package/utils"
import { NextResponse } from "next/server"

export const GET = withAuth(({ user }) => {
  try {
    if (!user.emailVerified) {
      throw new CustomError(
        "You must verify your email to access this resource.",
        400
      )
    }

    return NextResponse.json(user, { status: 200 })
  } catch (error) {
    return sendError(error, "Failed to get profile.", 500)
  }
})

The CustomError class takes two parameters:

  • message: The error message to send in the response.
  • status: The HTTP status code to return.

In the above example, if the user's email is not verified, a 400 Bad Request error is thrown with a custom error message. The sendError function in the catch clause then sends this error message in the response.

Last updated on