Design Converter
Education
Last updated on Apr 4, 2025
•8 mins read
Last updated on Apr 4, 2025
•8 mins read
Managing API requests can get complicated in modern web development. React apps often need to juggle multiple API calls, show loading states, process what comes back, and handle errors. That's where React Query middleware comes in handy. It gives you the tools to simplify all that.
In this blog, we'll walk you through building custom middleware with RTK Query, part of the Redux toolkit, to make API requests easier to handle.
RTK Query from redux toolkit offers a structured approach to API management. The middleware system in RTK Query acts as a pipeline that processes API requests before they reach the server and after the responses are returned.
Middleware functions in RTK Query intercept and process API requests and responses. They sit between your application and the API endpoints, allowing you to:
• Modify requests before they're sent
• Transform responses before they reach your components
• Handle errors consistently
• Log API activity
• Add authentication tokens
Let's visualize the middleware flow:
Before we dive into custom middleware, let's set up RTK Query in a React app.
First, install the necessary packages:
1import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' 2import { configureStore } from '@reduxjs/toolkit' 3import { setupListeners } from '@reduxjs/toolkit/query' 4 5// Create an API service 6const api = createApi({ 7 reducerPath: 'api', 8 baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }), 9 endpoints: (builder) => ({ 10 getUsers: builder.query({ 11 query: () => 'users', 12 }), 13 getUserById: builder.query({ 14 query: (id) => `users/${id}`, 15 }), 16 }), 17}) 18 19// Export hooks for usage in components 20export const { useGetUsersQuery, useGetUserByQueryQuery } = api 21 22// Configure the store 23const store = configureStore({ 24 reducer: { 25 [api.reducerPath]: api.reducer, 26 }, 27 middleware: (getDefaultMiddleware) => 28 getDefaultMiddleware().concat(api.middleware), 29}) 30 31// Enable refetchOnFocus and refetchOnReconnect 32setupListeners(store.dispatch)
In this setup, we've created an API slice with two endpoints and configured our redux store to use the default middleware plus the RTK Query middleware.
Now, let's explore how to create custom middleware for RTK Query to handle specific requirements.
An everyday use case for middleware is adding authentication tokens to requests:
1import { fetchBaseQuery } from '@reduxjs/toolkit/query/react' 2 3// Create a custom base query with auth headers 4const baseQueryWithAuth = fetchBaseQuery({ 5 baseUrl: 'https://api.example.com', 6 prepareHeaders: (headers, { getState }) => { 7 // Get token from state 8 const token = getState().auth.token 9 10 // Add token to headers if it exists 11 if (token) { 12 headers.set('authorization', `Bearer ${token}`) 13 } 14 15 return headers 16 }, 17}) 18 19// Use in API definition 20const api = createApi({ 21 reducerPath: 'api', 22 baseQuery: baseQueryWithAuth, 23 endpoints: (builder) => ({ 24 // endpoints here 25 }), 26})
This middleware automatically adds the authentication token to every request.
Another powerful application is centralized error handling:
1import { fetchBaseQuery } from '@reduxjs/toolkit/query/react' 2 3// Create error handling middleware 4const baseQueryWithErrorHandling = async (args, api, extraOptions) => { 5 const baseQuery = fetchBaseQuery({ baseUrl: 'https://api.example.com' }) 6 7 try { 8 // Attempt the request 9 const result = await baseQuery(args, api, extraOptions) 10 11 // Check for error status codes 12 if (result.error) { 13 // Handle specific error codes 14 if (result.error.status === 401) { 15 // Dispatch logout action or refresh token 16 api.dispatch({ type: 'auth/logout' }) 17 } 18 19 // Log all errors 20 console.error('API error:', result.error) 21 } 22 23 return result 24 } catch (error) { 25 // Handle unexpected errors 26 return { error: { status: 'FETCH_ERROR', error: String(error) } } 27 } 28} 29 30// Use in API definition 31const api = createApi({ 32 reducerPath: 'api', 33 baseQuery: baseQueryWithErrorHandling, 34 endpoints: (builder) => ({ 35 // endpoints here 36 }), 37})
This middleware catches and processes errors from API calls, providing a central place to handle authentication failures or server errors.
Let's explore more advanced middleware applications with RTK Query.
RTK Query includes built-in caching, but you can enhance it with custom middleware:
1import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' 2 3// Create API with custom cache invalidation 4const api = createApi({ 5 reducerPath: 'api', 6 baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }), 7 tagTypes: ['User', 'Post'], 8 endpoints: (builder) => ({ 9 getUsers: builder.query({ 10 query: () => 'users', 11 providesTags: ['User'], 12 }), 13 updateUser: builder.mutation({ 14 query: (user) => ({ 15 url: `users/${user.id}`, 16 method: 'PATCH', 17 body: user, 18 }), 19 invalidatesTags: ['User'], 20 }), 21 getPosts: builder.query({ 22 query: () => 'posts', 23 providesTags: ['Post'], 24 }), 25 }), 26})
This setup automatically invalidates the cache for user data when any user is updated, triggering a data refresh.
You can transform requests and responses with middleware:
1import { fetchBaseQuery } from '@reduxjs/toolkit/query/react' 2 3// Create transformation middleware 4const transformationMiddleware = async (args, api, extraOptions) => { 5 const baseQuery = fetchBaseQuery({ baseUrl: 'https://api.example.com' }) 6 7 // Transform request before sending 8 let transformedArgs = args 9 if (args.body) { 10 // Convert dates to ISO strings for API 11 transformedArgs = { 12 ...args, 13 body: JSON.parse(JSON.stringify(args.body).replace( 14 /"\d{4}-d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z"/g, 15 match => `"${new Date(JSON.parse(match)).toISOString()}"` 16 )) 17 } 18 } 19 20 // Make the request 21 const result = await baseQuery(transformedArgs, api, extraOptions) 22 23 // Transform response before returning 24 if (result.data) { 25 // Add computed values or transform data 26 return { 27 ...result, 28 data: Array.isArray(result.data) 29 ? result.data.map(item => ({ ...item, fullName: `${item.firstName} ${item.lastName}` })) 30 : { ...result.data, fullName: `${result.data.firstName} ${result.data.lastName}` } 31 } 32 } 33 34 return result 35} 36 37// Use in API definition 38const api = createApi({ 39 reducerPath: 'api', 40 baseQuery: transformationMiddleware, 41 endpoints: (builder) => ({ 42 // endpoints here 43 }), 44})
This middleware transforms outgoing and incoming requests, standardizing date formats and adding computed values.
For complex applications, you might need to combine multiple middleware functions:
1import { fetchBaseQuery } from '@reduxjs/toolkit/query/react' 2 3// Authentication middleware 4const withAuth = async (args, api, extraOptions) => { 5 const baseQuery = fetchBaseQuery({ 6 baseUrl: 'https://api.example.com', 7 prepareHeaders: (headers, { getState }) => { 8 const token = getState().auth.token 9 if (token) { 10 headers.set('authorization', `Bearer ${token}`) 11 } 12 return headers 13 }, 14 }) 15 16 return baseQuery(args, api, extraOptions) 17} 18 19// Logging middleware that wraps authentication middleware 20const withLogging = async (args, api, extraOptions) => { 21 console.log('Request:', args) 22 23 const startTime = Date.now() 24 const result = await withAuth(args, api, extraOptions) 25 const duration = Date.now() - startTime 26 27 if (result.error) { 28 console.error('Error:', result.error, `(${duration}ms)`) 29 } else { 30 console.log('Response:', result.data, `(${duration}ms)`) 31 } 32 33 return result 34} 35 36// Error handling middleware that wraps logging middleware 37const withErrorHandling = async (args, api, extraOptions) => { 38 try { 39 const result = await withLogging(args, api, extraOptions) 40 41 if (result.error) { 42 if (result.error.status === 401) { 43 api.dispatch({ type: 'auth/logout' }) 44 } 45 } 46 47 return result 48 } catch (error) { 49 return { error: { status: 'FETCH_ERROR', error: String(error) } } 50 } 51} 52 53// Use combined middleware in API definition 54const api = createApi({ 55 reducerPath: 'api', 56 baseQuery: withErrorHandling, 57 endpoints: (builder) => ({ 58 // endpoints here 59 }), 60})
This example chains three middleware functions together:
withAuth adds authentication
withLogging logs requests and responses
withErrorHandling handles errors
Let's see how middleware can solve real-world problems:
1import React from 'react' 2import { useGetUsersQuery } from './api' 3 4function UserList() { 5 const { data: users, isLoading, error } = useGetUsersQuery() 6 7 if (isLoading) { 8 return <div>Loading users...</div> 9 } 10 11 if (error) { 12 return <div>Error loading users: {error.message}</div> 13 } 14 15 return ( 16 <ul> 17 {users.map(user => ( 18 <li key={user.id}>{user.name}</li> 19 ))} 20 </ul> 21 ) 22}
RTK Query automatically tracks loading states, making it easy to show loading indicators.
1const api = createApi({ 2 reducerPath: 'api', 3 baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }), 4 tagTypes: ['Post'], 5 endpoints: (builder) => ({ 6 getPosts: builder.query({ 7 query: () => 'posts', 8 providesTags: ['Post'], 9 }), 10 updatePost: builder.mutation({ 11 query: (post) => ({ 12 url: `posts/${post.id}`, 13 method: 'PATCH', 14 body: post, 15 }), 16 async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) { 17 // Optimistically update the post in the cache 18 const patchResult = dispatch( 19 api.util.updateQueryData('getPosts', undefined, draft => { 20 const post = draft.find(post => post.id === id) 21 if (post) { 22 Object.assign(post, patch) 23 } 24 }) 25 ) 26 27 try { 28 // Wait for the request to complete 29 await queryFulfilled 30 } catch { 31 // If it fails, revert the optimistic update 32 patchResult.undo() 33 } 34 }, 35 invalidatesTags: ['Post'], 36 }), 37 }), 38})
This middleware applies optimistic updates, immediately updating the UI before the server confirms the change.
When implementing middleware, consider these performance tips:
RTK Query middleware provides a powerful system for handling API requests in React applications. You can centralize authentication, error handling, logging, and data transformation by implementing custom middleware, making your code more maintainable and robust.
The middleware approach in RTK Query offers a structured way to manage the complexity of API interactions, letting you focus on building features rather than managing request logic. Applying the techniques in this guide allows you to create more efficient and reliable React applications.
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.