When using an external backend with a Next.js application, maintaining centralized logic for API calls is crucial. This approach improves code maintainability, reusability, and consistency across both client and server components.
In this guide, we’ll create a centralized API service and show how to use it across different components in three simple steps. This blog will also help you gain a deeper understanding of data-fetching and caching principles tailored to various component types in Next.js.
Why Centralize API Logic?
- Maintainability: Centralized functions handle all API calls, making updates or bug fixes easy in a single location.
- Reusability: Write once, use anywhere—whether in server-side, static, or client-side components.
- Consistency: Centralized error handling ensures predictable responses across your app.
Step 1: Create the API Service Layer
Define API functions in a dedicated service file, services/userService.js
, to fetch data from your backend:
// services/userService.js
export async function fetchUserData(userId) {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user data: ${response.statusText}`);
}
return response.json();
}
Step 2: Use the Service in Server-Side Components
For server-rendered pages, call the service function directly within getServerSideProps
or getStaticProps
. This allows server-side data fetching with zero client-side JavaScript overhead.
// pages/user/[id].js
import { fetchUserData } from '@/services/userService';
export async function getServerSideProps({ params }) {
const { id } = params;
try {
const user = await fetchUserData(id);
return { props: { user } };
} catch (error) {
console.error(error);
return { notFound: true };
}
}
export default function UserPage({ user }) {
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Step 3: Use the Service in Client-Side Components
For dynamic or interactive client components, wrap the service function in a custom hook that handles loading and error states.
// hooks/useUser.js
import { useState, useEffect } from 'react';
import { fetchUserData } from '@/services/userService';
export function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!userId) return;
const getUserData = async () => {
setLoading(true);
try {
const data = await fetchUserData(userId);
setUser(data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
getUserData();
}, [userId]);
return { user, loading, error };
}
Then, use this custom hook in your client-side component:
// components/UserProfile.js
'use client';
import { useUser } from '@/hooks/useUser';
export default function UserProfile({ userId }) {
const { user, loading, error } = useUser(userId);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error loading user data</p>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Summary
Centralizing API calls in Next.js with an external backend simplifies data-fetching logic across server and client components. By organizing API logic in a service layer, we streamline updates, improve reusability, and ensure consistent error handling across your application. This setup keeps your code clean and maintainable, whether you’re fetching data on the server or client side.
Top comments (0)