Skip to main content

Overview

The Auth Dashboard follows a feature-based architecture with clear separation of concerns. The application is built with React, TypeScript, and modern tooling to ensure maintainability and scalability.

Project Structure

The codebase is organized into logical domains, making it easy to locate and modify functionality:
src/
├── app/                    # Application-level routing and guards
│   ├── router.tsx         # Main router configuration
│   ├── ProtectedRoute.tsx # Authentication guard for private routes
│   └── PublicRoute.tsx    # Guard for public-only routes (e.g., login)

├── features/              # Feature modules (domain-driven)
│   ├── auth/             # Authentication feature
│   │   ├── authStore.tsx     # Zustand store for auth state
│   │   ├── authService.ts    # API calls for authentication
│   │   ├── useAuth.ts        # Custom hook
│   │   └── types.ts          # TypeScript interfaces
│   │
│   └── users/            # User management feature
│       ├── usersStore.ts     # Zustand store for users state
│       ├── usersService.ts   # API calls for users
│       ├── types.ts          # TypeScript interfaces
│       └── components/       # Feature-specific components

├── pages/                 # Page components (route views)
│   ├── Login.tsx
│   └── dashboard/
│       ├── Dashboard.tsx
│       ├── Users.tsx
│       ├── UserDetails.tsx
│       └── Settings.tsx

├── shared/                # Shared resources across features
│   ├── components/       # Reusable UI components
│   ├── layout/           # Layout components
│   └── store/            # Global stores (toast, settings)

├── services/             # Core services
│   └── api.ts           # Axios instance with interceptors

└── i18n/                 # Internationalization
    └── index.ts

Architecture Principles

Feature-Based Organization

Each feature is self-contained with its own store, services, types, and components:
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { loginRequest } from "./authService";
import type { AuthState } from "./types";

export const useAuthStore = create<AuthState>()(\n  persist(
    (set) => ({
      user: null,
      loading: false,

      login: async (username, password) => {
        try {
          set({ loading: true });
          const data = await loginRequest({ username, password });
          set({ user: data, loading: false });
          localStorage.setItem("token", data.token);
        } catch (error) {
          set({ loading: false });
          console.error(error);
          alert("Credenciales incorrectas");
        }
      },

      logout: () => {
        localStorage.removeItem("token");
        set({ user: null });
      },
    }),
    { name: "auth-storage" }
  )
);

Separation of Concerns

Store

State management and business logic using Zustand

Service

API calls and data transformation logic

Types

TypeScript interfaces and type definitions

Components

UI components specific to the feature

Layered Architecture

The application follows a clear layered architecture:

Layer Responsibilities

Pages Layer (src/pages/)
  • Route-level components
  • Page composition and layout
  • Minimal business logic
  • Connects features together
Features Layer (src/features/)
  • Domain-specific logic
  • State management per feature
  • API integration for the feature
  • Feature-specific components
Services Layer (src/services/)
  • HTTP client configuration
  • Global interceptors
  • Shared utilities
Shared Layer (src/shared/)
  • Reusable UI components
  • Layout components
  • Global state (toast, settings)

State Management Strategy

The application uses Zustand for state management with the persist middleware for data persistence.
Each feature has its own store:
  • Auth Store (features/auth/authStore.tsx): User authentication state
  • Users Store (features/users/usersStore.ts): User management state
  • Toast Store (shared/store/useToastStore.ts): Global notifications
  • Settings Store (shared/store/useSettingsStore.ts): App preferences
Example store structure from features/users/usersStore.ts:18-66:
export const useUsersStore = create<UsersState>()(\n  persist(
    (set, get) => ({
      users: [],
      loading: false,
      error: null,
      deletingId: null,

      fetchUsers: async () => {
        if (get().users.length > 0) return;
        try {
          set({ loading: true, error: null });
          const usersFromApi = await getUsers();
          set({ users: usersFromApi, loading: false });
        } catch {
          set({ error: "Error fetching users", loading: false });
        }
      },

      addUser: (user) =>
        set((state) => ({
          users: [user, ...state.users],
        })),

      deleteUser: (id) => {
        set({ deletingId: id });
        set((state) => ({
          users: state.users.filter((user) => user.id !== id),
          deletingId: null,
        }));
      },

      updateUser: (updatedUser: User) =>
        set((state) => ({
          users: state.users.map((user) =>
            user.id === updatedUser.id ? updatedUser : user
          ),
        })),
    }),
    { name: "users-storage" }
  )
);

Routing Architecture

The router configuration uses nested routes with layout components. See Routing for detailed information.

API Integration

Centralized API configuration with axios interceptors for authentication. See API Integration for details.

Component Library

Shared components are built with composition in mind. See Components for the component library.

Best Practices

  • Keep features isolated and self-contained
  • Each feature should have its own types, store, service, and components
  • Don’t import from other features directly
  • Use shared components for common UI patterns
  • One store per feature domain
  • Use persist middleware for data that should survive page refreshes
  • Keep stores focused on a single responsibility
  • Implement optimistic updates where appropriate
  • Define types in separate types.ts files
  • Use interfaces for object shapes
  • Leverage type inference where possible
  • Export types for reuse across the feature
  • Feature-specific components go in features/{name}/components/
  • Reusable components go in shared/components/
  • Use composition over prop drilling
  • Keep components focused on a single responsibility

Adding a New Feature

To add a new feature to the application:
1

Create feature folder

Create a new folder under src/features/ with the feature name:
mkdir -p src/features/my-feature/components
2

Define types

Create types.ts with TypeScript interfaces:
export interface MyFeatureData {
  id: number;
  name: string;
}

export interface MyFeatureState {
  data: MyFeatureData[];
  loading: boolean;
  fetchData: () => Promise<void>;
}
3

Create service

Create myFeatureService.ts for API calls:
import { api } from "../../services/api";
import type { MyFeatureData } from "./types";

export const getMyFeatureData = async (): Promise<MyFeatureData[]> => {
  const { data } = await api.get("/my-feature");
  return data;
};
4

Create store

Create myFeatureStore.ts with Zustand:
import { create } from "zustand";
import { getMyFeatureData } from "./myFeatureService";
import type { MyFeatureState } from "./types";

export const useMyFeatureStore = create<MyFeatureState>((set) => ({
  data: [],
  loading: false,
  fetchData: async () => {
    set({ loading: true });
    const data = await getMyFeatureData();
    set({ data, loading: false });
  },
}));
5

Add routes

Add routes in src/app/router.tsx and create page components in src/pages/

Next Steps

State Management

Learn about Zustand stores and state patterns

Routing

Understand the routing structure and guards

API Integration

Configure API calls and interceptors

Components

Explore the shared component library

Build docs developers (and LLMs) love