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

by admin

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


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

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 (
) => {
  let result = await baseQueryWithAuth(args, api, extraOptions)
  if (result.error && result.error.status === 401) {
    // try to get a new token
    const refreshToken = localStorage.getItem('refreshToken')
    const refreshResult = await baseQueryWithAuth(
      { url: `/Auth/RefreshToken?refreshToken=${refreshToken}`, method: 'POST' },
    if (refreshResult.data) {
      const refeshTokenResult = refreshResult.data as any

      // store the new token
      localStorage.setItem('accessToken', refeshTokenResult.data.accessToken)
      localStorage.setItem('refreshToken', refeshTokenResult.data.refreshToken)

      // retry the initial query
      result = await baseQueryWithAuth(args, api, extraOptions)
    } else {

  if (result.error && (result.error.status === 500 || result.error.status === 400 || result.error.status === 403)) {
    const data = result?.error?.data as any
    if (data?.exception) {
      //Show more generic error when an error occurs.

  return result

export const apiGenerator = createApi({
  baseQuery: baseQueryWithReauth,
  endpoints: () => ({})

export const {} = apiGenerator

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 {
} 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 {
} = 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