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.