Skip to content

flexsw917/NextJs_frontend_template

Repository files navigation

Frontend Template

A production-ready Next.js template with authentication, protected routes, and modular architecture.

Tech Stack

  • Next.js 16 (App Router)
  • React 19
  • TypeScript 5
  • TanStack Query (React Query)
  • React Hook Form + Zod
  • Axios
  • next-themes
  • Tailwind CSS 4
  • ESLint 9 + Prettier
  • Untitled UI React

Getting Started

pnpm install
pnpm dev

Open http://localhost:3000 with your browser to see the result.

Scripts

pnpm dev          # Start development server
pnpm build        # Build for production
pnpm start        # Start production server
pnpm lint         # Run ESLint
pnpm format       # Format with Prettier
pnpm type-check   # TypeScript type checking

Environment Variables

Create a .env.local file:

NEXT_PUBLIC_API_BASE_URL=http://localhost:4000/api

# Optional: Token lifetimes in seconds
# ACCESS_TOKEN_MAX_AGE=900        # Default: 15 minutes
# REFRESH_TOKEN_MAX_AGE=604800    # Default: 7 days

Environment validation runs at startup. Missing required variables will throw an error.

Docker

Build and run with Docker Compose:

docker compose up --build

Or build manually:

docker build -t frontend-template --build-arg NEXT_PUBLIC_API_BASE_URL=http://localhost:4000/api .
docker run -p 3000:3000 frontend-template

Configure environment variables in docker-compose.yml or pass them at runtime.

Project Structure

src/
├── app/
│   ├── (exposed)/          # Public pages (accessible by everyone)
│   ├── (protected)/        # Authenticated users only (with server prefetch)
│   ├── auth/               # Non-authenticated users only
│   ├── api/                # API routes
│   ├── error.tsx           # Error boundary
│   └── global-error.tsx    # Global error boundary
├── components/
│   ├── base/
│   │   ├── _rhf/           # React Hook Form adapted components
│   │   └── [component]/    # Base UI components
│   ├── guards/             # Route guards (EmailVerifiedGuard)
│   ├── error-boundary.tsx  # Client error boundary
│   └── page-loading.tsx    # Full page loading
├── contexts/               # React contexts
├── libs/
│   ├── cookies/            # Cookie utilities
│   ├── server/             # Server-side utilities
│   ├── env.ts              # Environment validation
│   ├── query-keys.ts       # React Query key factory
│   └── validators/         # Zod validation schemas
├── modules/                # Feature modules
│   ├── auth/
│   └── users/
├── providers/              # App providers
├── services/               # Axios instances and token service
└── utils/                  # Utility functions

Naming Conventions

Type Convention Example
Files snake_case user_profile.tsx
Components PascalCase UserProfile
Hooks camelCase with prefix useUserProfile
Types PascalCase AuthUser
Validators camelCase signinValidator

Route Groups

(exposed)

Public pages accessible by everyone regardless of authentication status.

(protected)

Pages requiring authentication. Unauthenticated users are redirected to /auth/login with a redirect query parameter.

auth

Authentication pages (login, register, forgot-password, reset-password, verify-email). Authenticated users are redirected to /dashboard or the redirect query parameter.

Query Key Factory

Use the query key factory for consistent query keys:

import { queryKeys } from "@/libs/query-keys";

queryKeys.users.profile()     // ["users", "profile"]
queryKeys.users.detail("123") // ["users", "detail", "123"]

Extend for new modules:

export const queryKeys = {
    users: { ... },
    posts: {
        all: ["posts"] as const,
        list: () => [...queryKeys.posts.all, "list"] as const,
        detail: (id: string) => [...queryKeys.posts.all, "detail", id] as const,
    },
};

Authentication System

Token Storage

Tokens are stored in cookies with the following configuration:

Option Value Notes
httpOnly false Accessible to JavaScript for client-side auth checks
secure true (production) HTTPS only in production
sameSite lax CSRF protection
path / Available to all routes

Token Lifetimes (configurable via environment variables):

  • Access Token: ACCESS_TOKEN_MAX_AGE (default: 900 seconds / 15 minutes)
  • Refresh Token: REFRESH_TOKEN_MAX_AGE (default: 604800 seconds / 7 days)

Note: Cookies are intentionally NOT httpOnly to allow client-side JavaScript to read tokens for auth state management. The access token is included in the Authorization header for API requests.

Server-Side Prefetching

Protected routes prefetch user profile on the server:

// src/app/(protected)/layout.tsx
export default async function ProtectedLayout({ children }) {
    const queryClient = new QueryClient();
    const user = await getUserProfile();

    if (user) {
        queryClient.setQueryData(queryKeys.users.profile(), user);
    }

    return (
        <HydrationProvider state={dehydrate(queryClient)}>
            <EmailVerifiedGuard>{children}</EmailVerifiedGuard>
        </HydrationProvider>
    );
}

This eliminates loading states on initial page load.

Middleware Protection

Route protection is handled server-side in src/middleware.ts. Protected and auth routes are defined in arrays at the top of the file.

Token Flow

  1. Login/Register: Backend returns tokens → stored via /api/auth/token POST
  2. API Requests: Cookies sent automatically with withCredentials: true
  3. Token Refresh: On 401 response, /api/auth/refresh is called automatically
  4. Logout: Tokens cleared via /api/auth/token DELETE

Adding Protected Routes

Edit src/middleware.ts:

const PROTECTED_ROUTES = ["/dashboard", "/settings", "/your-new-route"];

Module Structure

Each module follows a layered architecture:

modules/[module-name]/
├── api/
│   └── [module]API.ts      # API endpoint functions
├── services/
│   └── use[Action].ts      # TanStack Query hooks
├── ui/
│   └── [Component].tsx     # UI components
└── types.ts                # Module-specific types

Creating a New Module

  1. Create module folder: src/modules/[module-name]/
  2. Add API layer:
// src/modules/posts/api/postsAPI.ts
import axiosInstance from "@/services";

export const postsAPI = {
    getAll: async () => {
        const response = await axiosInstance.get("/posts");
        return response.data;
    },
    getById: async (id: string) => {
        const response = await axiosInstance.get(`/posts/${id}`);
        return response.data;
    },
    create: async (data: CreatePostData) => {
        const response = await axiosInstance.post("/posts", data);
        return response.data;
    },
};
  1. Add query keys to the factory:
// src/libs/query-keys.ts
export const queryKeys = {
    // ... existing keys
    posts: {
        all: ["posts"] as const,
        list: () => [...queryKeys.posts.all, "list"] as const,
        detail: (id: string) => [...queryKeys.posts.all, "detail", id] as const,
    },
};
  1. Add service hooks:
// src/modules/posts/services/usePosts.ts
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "@/libs/query-keys";
import { postsAPI } from "../api/postsAPI";

export const usePosts = () => {
    return useQuery({
        queryKey: queryKeys.posts.list(),
        queryFn: postsAPI.getAll,
    });
};
  1. Add mutation hooks:
// src/modules/posts/services/useCreatePost.ts
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/libs/query-keys";
import { postsAPI } from "../api/postsAPI";

export const useCreatePost = () => {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn: postsAPI.create,
        onSuccess: () => {
            queryClient.invalidateQueries({ queryKey: queryKeys.posts.all });
        },
    });
};
  1. Add types:
// src/modules/posts/types.ts
export type Post = {
    id: string;
    title: string;
    content: string;
    created_at: string;
};

export type CreatePostData = {
    title: string;
    content: string;
};

Validators

Validators use Zod v4 and are located in src/libs/validators/.

// src/libs/validators/create_post.ts
import { z } from "zod";

export const createPostValidator = z.object({
    title: z.string({ error: "Title is required" }).min(1, "Title is required"),
    content: z.string({ error: "Content is required" }).min(1, "Content is required"),
});

export type CreatePostFormData = z.infer<typeof createPostValidator>;

Usage with React Hook Form:

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { createPostValidator, CreatePostFormData } from "@/libs/validators/create_post";

const form = useForm<CreatePostFormData>({
    resolver: zodResolver(createPostValidator),
});

React Hook Form Components

RHF-adapted components are in src/components/base/_rhf/. They wrap base components with useController.

Available components:

  • RHFInput - Text input
  • RHFPasswordInput - Password input with visibility toggle
  • RHFTextarea - Textarea
  • RHFCheckbox - Checkbox
  • RHFNumberInput - Number input with increment/decrement

Usage:

import { RHFInput } from "@/components/base/_rhf/rhf-input";

<RHFInput
    name="email"
    control={form.control}
    label="Email"
    placeholder="Enter your email"
/>

Axios Instances

Two axios instances are available in src/services/index.ts:

  • axiosInstance (default export): For authenticated requests. Includes 401 interceptor that automatically refreshes tokens.
  • authAxiosInstance: For auth endpoints (login, register). No token refresh interceptor.

Theme System

Theme toggling uses next-themes with class-based switching:

  • Light mode: light-mode class
  • Dark mode: dark-mode class

ThemeToggler component available at src/components/base/theme-toggler/.

Error Handling

Error Boundaries

  • src/app/error.tsx - Catches errors in route segments
  • src/app/global-error.tsx - Catches errors in root layout
  • src/components/error-boundary.tsx - Client-side error boundary

API Error Handling

Use getErrorMessage for consistent error messages:

import { getErrorMessage } from "@/modules/common/libs/getErrorMessage";

onError: (error) => {
    toast.error(getErrorMessage(error, "Failed to save"));
}

Code Style

  • No comments in code
  • Prefer editing existing files over creating new ones
  • Keep components focused and single-purpose
  • Use TypeScript strict mode
  • Run pnpm lint before committing

Resources

This template uses Untitled UI React components.

About

NextJS, + all react template features

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages