Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Next.js Architecture

This document outlines the architecture of the project, including the components, their responsibilities, and how they interact with each other.

Table of Contents

  1. Structure Overview
  2. Folder Structure
  3. Applied Architectural Principles

Structure Overview

.
├── src
│   ├── lib
│   │   ├── features
│   │   │   ├── features-1
│   │   │   │   ├── components
│   │   │   │   │   ├── ...
│   │   │   │   └── services
│   │   │   │       ├── __tests__/
│   │   │   │       │   └── test.ts
│   │   │   │       ├── daos.ts
│   │   │   │       ├── models.ts
│   │   │   │       └── queries.ts
│   │   │   ├── feature-2
│   │   │   │   ├── components
│   │   │   │   │   └── dashboard
│   │   │   │   │       ├── ...
│   │   │   │   └── services
│   │   │   │       ├── __tests__/
│   │   │   │       │   └── test.ts
│   │   │   │       ├── daos.ts
│   │   │   │       ├── models.ts
│   │   │   │       └── queries.ts
│   │   │   ├── shared/
│   │   │   │   ├── components/
│   │   │   │   │   ├── app-navbar.tsx
│   │   │   │   │   └── _shadcn
│   │   │   │   │       ├── ...
│   │   │   │   └── services/
│   │   │   │       ├── __tests__/
│   │   │   │       │   └── test.ts
│   │   │   │       ├── daos.ts
│   │   │   │       ├── models.ts
│   │   │   │       └── queries.ts
│   │   ├── config/
│   │   │   ├── ...
│   ├── app/
│   │   ├── page.tsx
│   │   └── ...

Folder Structure

src/lib/

Contains all reusable logic, including components, services, utilities, and configurations. Organized as follows:

features/ – Feature-Specific Implementations

Implementations specific of UI components, services, hooks and more utilities to real application features (e.g. features/user, features/admin):

  • components/ – Generic Components

    Contains UI components specific to the feature. This directory does not follow any specific pattern in order to allow for maximum flexibility to organize components as needed.

  • services/ – Data Access Layer

    Each service follows the DAO + TanStack + Zod pattern:

    • __tests__/
      Unit tests for the service.

      • test.ts file is the entry point for the tests.
    • daos.ts
      Pure functions for direct data access (e.g., APIs, local databases).

      // lib/services/user/dao.ts
      import type { UserModel } from "./models";
      
      export async function fetchUser(id: string): Promise<User> {
        const res = await fetch(`/api/users/${id}`);
        if (!res.ok) throw new Error("Failed to fetch user");
        return res.json();
      }
      
      export async function updateUser(
        id: string,
        data: UserModel,
      ): Promise<UserModel> {
        const res = await fetch(`/api/users/${id}`, {
          method: "PUT",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(data),
        });
        if (!res.ok) throw new Error("Failed to update user");
        return res.json();
      }
      
    • queries.ts
      TanStack Query hooks that wrap dao.ts with state and cache logic.

      // lib/services/user/queries.ts
      import {
        useQuery,
        useMutation,
        useQueryClient,
      } from "@tanstack/react-query";
      import { fetchUser, updateUser } from "./dao";
      import { userModelSchema } from "./models";
      import type { User } from "./models";
      
      // For building good query key factories, see:
      // https://tkdodo.eu/blog/effective-react-query-keys
      export const userQueryKeys = {
        all: ["user"] as const,
        detail: (id: string) => [...userQueryKeys.all, "detail", id] as const,
      };
      
      export function useGetUserById(userId: string) {
        return useQuery({
          queryKey: userQueryKeys.detail(userId),
          queryFn: () => fetchUser(userId),
          enabled: !!userId, // only fetch if userId is defined
        });
      }
      
      export function useUpdateUser() {
        const queryClient = useQueryClient();
      
        return useMutation({
          mutationFn: async (user: User) => {
            const parsed = userSchema.parse(user);
            return updateUser(parsed.id, parsed);
          },
          onSuccess: (_data, user) => {
            queryClient.invalidateQueries({
              queryKey: userQueryKeys.detail(user.id),
            });
          },
        });
      }
      
    • models.ts Defines data schemas and models using Zod for runtime validation and static type inference

      // lib/services/users/models.ts
      import { z } from "zod";
      
      export const userModelSchema = z.object({
        id: z.string(),
        name: z.string(),
        email: z.string().email(),
        createdAt: z.string(),
      });
      
      export type UserModel = z.infer<typeof userModelSchema>;
      

    Usage Example:

    // pages/user/[id].tsx
    import { useRouter } from "next/router";
    import { useGetUserById, useUpdateUser } from "@/lib/services/user/queries";
    import { useState, useEffect } from "react";
    
    export default function UserPage() {
      const router = useRouter();
      const { id } = router.query as { id: string };
    
      const { data: user, isLoading } = useGetUserById(id);
      const { mutate: updateUser, isPending } = useUpdateUser();
    
      const [form, setForm] = useState({ name: "", email: "" });
    
      useEffect(() => {
        if (user) {
          setForm({ name: user.name, email: user.email });
        }
      }, [user]);
    
      const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setForm({ ...form, [e.target.name]: e.target.value });
      };
    
      const handleSubmit = () => {
        if (user) {
          updateUser({ ...user, ...form });
        }
      };
    
      if (isLoading) return <p>Carregando...</p>;
      if (!user) return <p>Usuário não encontrado</p>;
    
      return (
        <div>
          <h1>Editar Usuário</h1>
          <input name="name" value={form.name} onChange={handleChange} />
          <input name="email" value={form.email} onChange={handleChange} />
          <button onClick={handleSubmit} disabled={isPending}>
            Salvar
          </button>
        </div>
      );
    }
    

    NOTE: Form handling is not properly implemented in this example. On a real-world application, you should use a form library React Hook Form.

features/shared/ – Shared implementations

Follows the same pattern as features/, but is intended for implementations that are shared across multiple features.

utils/ – Generic Utilities

Pure helper functions such as:

  • Formatting
  • Validation
  • Conversions

config/ – Global Configurations

Defines aspects like:

  • Base API URL
  • Environment variables

src/app/

Standard NextJS app/ folder.

Responsible for:

  • Rendering actual pages.
  • Composing views using modules from src/lib/.

Applied Architectural Principles

Feature-Based Structure

Organize components and services per domain (e.g.: user/, admin/), not by type.

DAO Pattern

  • dao.ts contains ONLY external data access logic.
  • It DOES NOT process state, UI or business logic.

TanStack Query Integration

  • queries.ts encapsulates hooks that uses dao.ts.
  • Controls caching logic, onSuccess and onError callback management.