GitHub

Services Layer

How to organize all backend communication in one place — so when an API changes, you fix it in one file, not twenty.

01

Principle

When you fetch data directly inside a component, the component becomes responsible for knowing the URL, the HTTP method, the request format, and the error handling. That is four responsibilities too many. A services layer centralizes all backend communication — components just call a function and get data back. When the API changes, you fix it in one file, not twenty.

lightbulb

A service function should read like plain English: getUserById(id), createOrder(data), deletePost(id). If it needs more than one argument object, consider splitting it into two functions.

02

Rules

  • check_circle
    Services only talk to the APIA service function takes inputs, calls the API, and returns data. It does not touch state, does not render anything, and does not know about React.
  • check_circle
    One file per resourceGroup service functions by the API resource they belong to: users.ts, orders.ts, recipes.ts. Not by HTTP method.
  • check_circle
    Services live in lib/The services layer belongs in src/lib/ alongside the API client and query keys — not inside a feature folder.
  • check_circle
    Hooks consume services, components consume hooksComponents never call service functions directly. The chain is: service → custom hook → component.
03

Pattern

lib/services/users.ts — service layer pattern
import { apiClient } from '@/lib/api-client';
import type { User, CreateUserInput, UpdateUserInput } from '@/shared/types/user';

// ✅ Service functions — pure API communication
export const usersService = {
  getAll: async (): Promise<User[]> => {
    const response = await apiClient.get('/users');
    return response.data;
  },

  getById: async (id: string): Promise<User> => {
    const response = await apiClient.get(`/users/${id}`);
    return response.data;
  },

  create: async (data: CreateUserInput): Promise<User> => {
    const response = await apiClient.post('/users', data);
    return response.data;
  },

  update: async (id: string, data: UpdateUserInput): Promise<User> => {
    const response = await apiClient.patch(`/users/${id}`, data);
    return response.data;
  },

  delete: async (id: string): Promise<void> => {
    await apiClient.delete(`/users/${id}`);
  },
};
04

Implementation

info

Version Compatibility

Requires React 19+ and the latest stable versions of all dependencies shown.

In Next.js App Router, service functions can be called directly in Server Components. For Client Components, wrap them in React Query hooks.

lib/services/users.ts + hooks usage
// lib/api-client.ts — axios instance
import axios from 'axios';

export const apiClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  headers: { 'Content-Type': 'application/json' },
});

// lib/services/users.ts — service layer
import { apiClient } from '@/lib/api-client';
import type { User } from '@/shared/types/user';

export const usersService = {
  getAll: async (): Promise<User[]> => {
    const { data } = await apiClient.get('/users');
    return data;
  },
  getById: async (id: string): Promise<User> => {
    const { data } = await apiClient.get(`/users/${id}`);
    return data;
  },
};

// features/examples/hooks/useUsers.ts — hook wraps service
import { useQuery } from '@tanstack/react-query';
import { usersService } from '@/lib/services/users';

export function useUsers() {
  return useQuery({
    queryKey: ['users', 'list'],
    queryFn: usersService.getAll,
  });
}
menu_book
React Patterns

Helping developers build robust React applications since 2025.

© 2025 React Patterns Cookbook. Built with ❤️ for the community.
react-principles