How I Bypassed Token Expiry Hassles in Next.js with a Surprisingly Simple Trick

How I Bypassed Token Expiry Hassles in Next.js with a Surprisingly Simple Trick

At first, everything seemed perfect.

Our backend was locked down and clean—JWT-based auth using short-lived access tokens and long-lived refresh tokens. Thanks to HTTP-only cookies, it even felt secure. I’d log in, and every API request quietly carried the credentials in the background.

But after a while, I started noticing something annoying: 401s and 403s were popping up randomly across pages. Not while logging in or fetching protected data right away—but later. After some idle time, especially when I left a tab open and came back after a coffee.

A classic case of token expiry.

And that’s when the cracks began to show.

The First Fix That Wasn’t

My first attempt to fix this was… well, kind of primitive.

Every time I made an API call, I wrapped it in a try-catch. In the catch, if I hit a 401 or 403, I’d call the /api/auth/refresh endpoint to get a new access token, then retry the original request. Something like this:

try {
  await axiosClient.get("/api/some-protected-route");
} catch (err) {
  if (err.response.status === 401) {
    await refreshTokens();
    await axiosClient.get("/api/some-protected-route");
  }
}

But here’s the problem: this code lives everywhere. Every API call has this annoying boilerplate logic. What if I forget it somewhere? Or worse, what if two calls fire at once and race conditions kick in?

This was fragile. Messy. And definitely not scalable.

The Aha Moment

At some point I asked myself: “Why am I handling this in 50 different places? Can’t I just do it once?”

That led me to Axios Interceptors—a feature I’d used before but never leveraged this deeply.

Interceptors allow you to catch failed requests, inspect them, and even retry them automatically. So I set out to build one that:
1. Intercepts only 401 responses,
2. Skips intercepting if the failed request was itself to /refresh,
3. Calls /refresh to get a new access token via cookie,
4. Replays the original request, seamlessly.

That way, my app could recover from expired tokens silently, without users even noticing.

The Final Weapon

Here’s the actual interceptor I wired up:

import axios, { AxiosError } from "axios";
import { refreshTokens } from "../helper";

export const axiosClient = axios.create({
  baseURL: "http://localhost:3000",
  headers: {
    "Content-Type": "application/json",
  },
  withCredentials: true,
});

axiosClient.interceptors.response.use(
  (response) => response,
  async (error: AxiosError) => {
    if (
      error?.response?.status === 401 &&
      !error.request?.responseURL?.includes("/api/auth/refresh")
    ) {
      try {
        const res = await refreshTokens(); // hit /refresh to get new token
        if (error.config) {
          // retry original request
          return axiosClient.request({
            ...error.config,
            headers: { ...error.config.headers, ...res.headers },
          });
        }
      } catch (refreshTokenError) {
        return Promise.reject(refreshTokenError);
      }
    }
    return Promise.reject(error);
  }
);

And in helper.ts:

import axiosClient from "./axios";

export const refreshTokens = async () => {
  return axiosClient.get("/api/auth/refresh");
};

The key here is the logic inside the error interceptor:
✅ If a 401 happens, try refreshing the token.
✅ If the refresh is successful, re-send the original request.
✅ If refresh fails (maybe user is logged out), reject and let the app handle logout.

The Peace After the Storm

Since adding this interceptor, no more logout surprises.
No more catch blocks.
No more duplicated logic.

Everything just works. Even if a user leaves a tab open for hours, the first request after returning triggers the interceptor, refreshes the token, and silently resumes normal flow.

It feels like magic—but it’s just clean architecture.

TL;DR — Why This Matters

    • Centralized logic keeps your codebase clean.
    • WithCredentials + cookies allow secure token management.
    • Axios interceptors are powerful and underused.
    • This trick automatically bypasses token expiry without annoying your users.

If you’re using cookie-based JWT auth with refresh tokens in a Next.js app, and still sprinkling token logic everywhere—stop. Trust the interceptor. Let Axios carry the burden.

Want to see this in action with real auth flows?
I’m planning a follow-up walkthrough—subscribe or follow if you don’t want to miss it.

Happy coding. And may your tokens never expire unhandled. 🛡️

Read more