DEV Community

Cover image for 🚨 Stop Writing Messy API Calls in Next.js β€” Do This Instead (Production-Grade Setup)
codingKrills
codingKrills

Posted on

🚨 Stop Writing Messy API Calls in Next.js β€” Do This Instead (Production-Grade Setup)

Let me be brutally honest.

Most Next.js codebases I’ve reviewed lately are a complete mess when it comes to API handling:

  • useEffect everywhere
  • API calls inside components
  • No caching
  • Broken auth (random logouts 🀑)
  • Copy-pasted logic across files

And the worst part?

πŸ‘‰ These apps work… until they scale.


⚠️ The Problem Nobody Talks About

Frontend devs focus a lot on UI…

But ignore architecture.

Meanwhile, backend engineers:

  • design layers
  • enforce contracts
  • optimize performance

πŸ‘‰ Frontend?
"Just fetch it bro" πŸ˜…


πŸ’‘ So I Built This Instead

A production-grade API layer using modern tools:

🧱 Stack

  • Next.js (App Router)
  • Axios (client-side)
  • TanStack Query (API state)
  • Zustand (global state)
  • Zod (validation)
  • React Hook Form (forms)
  • JWT Auth (with refresh token rotation)

🧠 Architecture Overview

[ UI Components ]
        ↓
[ Custom Hooks (React Query) ]
        ↓
[ Services Layer ]
        ↓
[ Axios / Fetch Layer ]
        ↓
[ NestJS Backend ]
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Clean separation = scalable system


πŸ”₯ Step 1: STOP Calling APIs in Components

❌ Bad

useEffect(() => {
  fetch("/api/users").then(...)
}, [])
Enter fullscreen mode Exit fullscreen mode

βœ… Good (Service Layer)

// services/user.service.ts
export const getUsers = async () => {
  const res = await api.get("/users");
  return res.data;
};
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Your components should NEVER know how APIs work.


⚑ Step 2: React Query = Game Changer

Using TanStack Query:

const { data, isLoading } = useQuery({
  queryKey: ["users"],
  queryFn: getUsers,
});
Enter fullscreen mode Exit fullscreen mode

What you get for free:

  • πŸš€ caching
  • πŸ” retries
  • ⚑ background refetch
  • πŸ“Š pagination support

πŸ‘‰ This replaces Redux (for API state)


🧩 Step 3: Axios with Interceptors (REAL AUTH)

// lib/api.ts
import axios from "axios";

export const api = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  withCredentials: true,
});
Enter fullscreen mode Exit fullscreen mode

πŸ” Token Handling (IMPORTANT)

Flow:

Request β†’ 401 β†’ Refresh Token β†’ Retry Request
Enter fullscreen mode Exit fullscreen mode

Interceptor:

api.interceptors.response.use(
  res => res,
  async (error) => {
    if (error.response.status === 401) {
      const newToken = await refreshToken();
      error.config.headers.Authorization = `Bearer ${newToken}`;
      return api(error.config);
    }
    return Promise.reject(error);
  }
);
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ This is what separates amateur apps vs production apps


🧠 Step 4: Zustand (Keep It Simple)

Using Zustand:

import { create } from "zustand";

export const useAuthStore = create((set) => ({
  user: null,
  token: null,
  setAuth: (data) => set(data),
  logout: () => set({ user: null, token: null }),
}));
Enter fullscreen mode Exit fullscreen mode

βœ” no boilerplate
βœ” super fast
βœ” perfect with React Query


πŸ›‘οΈ Step 5: Zod = No More API Surprises

Using Zod:

const userSchema = z.object({
  id: z.string(),
  name: z.string(),
});
Enter fullscreen mode Exit fullscreen mode

Why this matters:

Without validation:

Backend changes β†’ frontend silently breaks 😬

With Zod:
βœ” strict validation
βœ” type safety
βœ” safer production


🧾 Step 6: Forms That Don’t Lag

Using React Hook Form:

const { register, handleSubmit } = useForm();

<form onSubmit={handleSubmit(onSubmit)}>
  <input {...register("email")} />
</form>
Enter fullscreen mode Exit fullscreen mode

βœ” minimal re-renders
βœ” clean integration with Zod
βœ” better UX


πŸ“ Step 7: Folder Structure (This is KEY)

/src
  /lib
    api.ts
    fetcher.ts
  /services
    auth.service.ts
    user.service.ts
  /hooks
    useApiQuery.ts
    useApiMutation.ts
  /store
    auth.store.ts
  /schemas
    user.schema.ts
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ This is what makes your app scalable after 6+ months.


🧠 Step 8: Custom Hooks = Clean UI

export const useApiQuery = (key, fn) => {
  return useQuery({
    queryKey: key,
    queryFn: fn,
  });
};
Enter fullscreen mode Exit fullscreen mode

Now in UI:

const { data } = useApiQuery(["users"], getUsers);
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Clean AF.


πŸ“Š Diagram: Full Flow

[Component]
   ↓
[useApiQuery]
   ↓
[Service Layer]
   ↓
[Axios Instance]
   ↓
[Interceptor]
   ↓
[NestJS API]
Enter fullscreen mode Exit fullscreen mode

πŸ’₯ The Real Difference

Before:

  • messy code
  • duplicated logic
  • no caching
  • auth bugs

After:

  • scalable architecture
  • centralized API logic
  • automatic caching
  • seamless auth flow

⚠️ Controversial Take

If you're still using useEffect for API calls in 2025…

You're writing legacy code.

Yeah, I said it.


🧠 Final Thoughts

Frontend is no longer β€œjust UI”.

It’s:

  • data orchestration
  • caching strategy
  • auth handling
  • performance optimization

πŸ‘‰ Treat it like backend architecture.


πŸš€ Want This Setup Instantly?

I created a Cursor AI command that generates this entire architecture automatically.

Drop a comment: "setup"
I’ll share it with you πŸ‘‡


πŸ”₯ Tags

nextjs #nestjs #reactquery #zustand #webdev #typescript #frontend #fullstack #javascript


πŸš€ Want the full setup?

I’ve created a Cursor AI command that generates this entire architecture automatically.

Comment β€œsetup” and I’ll share it πŸ‘‡

Top comments (0)