RTK Query – standard setup for headers, bearer auth, responses, errors

by admin
rtk-query

Learn how to create a RTK Query standard setup implementation that handles the following for you:

  • Set standard request headers
  • Add bearer authentication
  • JWT token refreshing
  • Showing messages for generic errors

This is probably the first thing I set up in any new project. This default RTK query implementation saves me a lot of time and provides a very generic entry point for any modifications you might need.

If you don’t know what RTK query is, learn more here: https://redux-toolkit.js.org/rtk-query/overview.

My file structure looks like this:

Root -> Redux -> api -> apiGenerator.ts (the apiGenerator.ts will hold the RTK query implementation).

apiGenerator.ts

import { BaseQueryFn, FetchArgs, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react'
import { loggedOut } from '../slices/authStore'

let isRefreshing = false // Flag to indicate if token refresh is in progress
let refreshPromise: any = null // To hold the refresh token promise

const baseQueryWithAuth = fetchBaseQuery({
  baseUrl: process.env.NEXT_PUBLIC_API_URL,
  prepareHeaders: async headers => {
    const accessToken = localStorage.getItem('accessToken')

    if (accessToken) {
      headers.set('Authorization', `Bearer ${accessToken}`)
    }

    headers.set('Accept', 'application/json')
    headers.set('Cache-Control', 'no-cache')
    headers.set('Pragma', 'no-cache')
    headers.set('Expires', '0')

    return headers
  }
})

const baseQueryWithReauth: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError> = async (
  args,
  api,
  extraOptions
) => {
  let result = await baseQueryWithAuth(args, api, extraOptions)
   if (result.error && result.error.status === 401) {
    if (!isRefreshing) {
      isRefreshing = true // Indicate refresh is in progress
      refreshPromise = Promise.resolve(
        baseQueryWithAuth(
          {
            url: `/v1/common/authenticate/RefreshToken`,
            method: 'POST',
            body: {
              token: localStorage.getItem('refreshToken')
            }
          },
          api,
          extraOptions
        )
      )
        .then(refreshResult => {
          if (refreshResult.data) {
            const refreshTokenResult = refreshResult.data as any

            // Store the new tokens
            localStorage.setItem('accessToken', refreshTokenResult.data.accessToken)
            localStorage.setItem('refreshToken', refreshTokenResult.data.refreshToken)
            isRefreshing = false // Reset the flag

            return refreshTokenResult
          } else {
            // Handle refresh error
            isRefreshing = false // Ensure flag is reset for future requests
            if (refreshResult?.error?.status === 500) {
              api.dispatch(loggedOut())
              location.reload()
            }

            return null
          }
        })
        .catch((error: any) => {
          console.error('Error refreshing token', error)
          isRefreshing = false // Reset flag on error
        })
    }

    await refreshPromise // Wait for the refresh token request to complete
    result = await baseQueryWithAuth(args, api, extraOptions) // Retry the initial query
  }

  return result
}
export const {} = baseQueryWithReauth

So, to explain from top to bottom.

baseUrl = the baseUrl is retrieved from the .env file to account for different environments. Just replace it with a hardcoded URL if needed.

prepareHeaders = This part of the setup retrieves the bearer token from localstorage and adds it as a Bearer header. In this section you may also define any other default headers.

The next section handles the actual API call and checks the responses. It checks for the following responses:

401 = a 401 response from the API means that you are not authroized to make that request. This often happens beacuse the access token is expired. Therefore it hits the /Auth/RefreshToken endpoint of our API to retrieve a new refresh token. You can learn more about that here.

400, 403, 500 = This part checks for the more common errors from an API and allows you to show a more generic error in your application. This is very useful because you often don’t want to show error details when the API throws 500 errors, but you want to show something more generic like ‘oops, something went wrong, please try again later’.

Define other API endpoints

Now that we have setup our base handler, we may add other endpoints. I often use the following structure for that.

Root -> Redux -> Slices -> {slicename}.ts.

So, for example, we have a login, forgot password and active account endpoint in our API. I create one slice to handle the authentication relevant endpoints. So I add a slice called ‘AuthSlice.ts’, with the following content:

import { apiGenerator } from '../api/apiGenerator'
import {
  LoginResult,
  LoginData,
  ActivateAccountInput,
  ResetPasswordInput
} from '../../types/auth'

const generatedAPI = apiGenerator.injectEndpoints({
  endpoints: builder => ({
    login: builder.mutation<LoginResult, LoginData>({
      query: data => ({
        url: `/Auth/Authenticate`,
        method: 'POST',
        body: data
      })
    }),
    resetPassword: builder.mutation<void, ResetPasswordInput>({
      query: data => ({
        url: `/v1/Account/ResetPassword`,
        method: 'POST',
        body: data
      })
    }),
    activateAccount: builder.mutation<void, ActivateAccountInput>({
      query: data => ({
        url: `/v1/Account/ActivateAccount`,
        method: 'POST',
        body: data
      })
    }),
  }),
  overrideExisting: true
})

export const {
  useLoginMutation,
  useResetPasswordMutation,
  useActivateAccountMutation,
} = generatedAPI

As you can see, the apiGenerator we just defined is imported at the top and we use the apiGenerator.injectEndpoints method to ‘append’ the endpoints to the base selection that we created. And that’s it pretty much it, we have created a RTK Query standard setup. Our base url, headers, bearer token, response handling area all automatically applied to all endpoints that we define.

If you have any questions or suggestions, feel free to leave them below and i’ll get back to you asap.

Related Posts

Leave a Comment