This Next.js frontend maintains absolute separation from backend concerns. All business logic, database access, and server-side operations live in the separate Node.js/Express backend.
Frontend (Next.js) Backend (Node.js/Express)
β β
β HTTP API Calls β
β βββββββββββββββββββββββββββΊ β
β β
β JSON Responses β
β βββββββββββββββββββββββββββ β
β β
- β UI components and pages
- β Client-side state management
- β Form validation (Zod schemas)
- β
API communication via
api-client.ts - β TypeScript types for data contracts
- β User authentication state (token storage)
- β Database connections or queries
- β Business logic beyond UI concerns
- β Direct data manipulation
- β Authentication token generation
- β Server-side secrets or credentials
src/
βββ app/ # Next.js App Router pages
β βββ (auth)/ # Auth pages (login, register)
β βββ (main)/ # Main app pages
β βββ layout.tsx # Root layout
β
βββ components/ # Reusable UI components
β βββ auth/ # Auth-related components
β βββ ui/ # shadcn/ui components
β βββ ... # Feature components
β
βββ hooks/ # Custom React hooks
β βββ use-courses.ts # Example: courses data hook
β
βββ lib/ # Utilities and helpers
β βββ api-client.ts # β Core API communication layer
β βββ services/ # Backend API service wrappers
β β βββ auth.service.ts
β β βββ courses.service.ts
β βββ utils.ts # Utility functions
β
βββ schemas/ # Zod validation schemas
β βββ auth.schema.ts # Form validation only
β
βββ store/ # Client state management (Zustand/Redux)
β
βββ types/ # TypeScript type definitions
βββ user.ts # DTOs matching backend models
The central HTTP client that handles all backend communication:
import { api } from "@/lib/api-client";
// Automatically includes:
// - Base URL from NEXT_PUBLIC_API_URL
// - Authorization header (if token exists)
// - Error handling
// - Type safety
const response = await api.get<ResponseType>("/endpoint");Service files wrap API endpoints with typed interfaces:
// lib/services/auth.service.ts
export const authService = {
async login(email: string, password: string) {
return api.post<LoginResponse>("/auth/login", { email, password });
},
};Why?
- Centralizes API endpoint definitions
- Provides type safety for requests/responses
- Makes refactoring easier
- Clear contract with backend
React hooks consume services and manage loading/error states:
// hooks/use-courses.ts
export function useCourses(options) {
const [courses, setCourses] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
coursesService.getCourses(options)
.then(response => setCourses(response.data.courses));
}, [options]);
return { courses, isLoading };
}// components/courses-list.tsx
export function CoursesList() {
const { courses, isLoading } = useCourses({ page: 1 });
if (isLoading) return <Spinner />;
return <div>{courses.map(course => <CourseCard {...course} />)}</div>;
}- User submits credentials β
LoginFormcomponent - Form validates β Zod schema (
schemas/auth.schema.ts) - Call auth service β
authService.login(email, password) - API client sends request β
POST http://localhost:5000/api/auth/login - Backend responds β
{ success: true, data: { user, token } } - Store token & user β localStorage/sessionStorage
- Navigate to dashboard β Based on user role
The API client automatically includes the token:
// api-client.ts automatically does this:
const token = localStorage.getItem("lms_token");
headers["Authorization"] = `Bearer ${token}`;# .env.local
# Frontend app URL
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Backend API URL (MUST have NEXT_PUBLIC_ prefix for client access)
NEXT_PUBLIC_API_URL=http://localhost:5000/api
# Cloudinary (frontend only - for upload widget)
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your-cloud-nameImportant:
- β
NEXT_PUBLIC_*variables are exposed to the browser - β Never put secrets in
NEXT_PUBLIC_*variables - β No
MONGODB_URI,JWT_SECRET, or API keys here
export interface Review {
_id: string;
courseId: string;
userId: string;
rating: number;
comment: string;
createdAt: string;
}import { api, ApiResponse } from "@/lib/api-client";
import { Review } from "@/types/review";
export const reviewsService = {
async getReviews(courseId: string) {
return api.get<ApiResponse<{ reviews: Review[] }>>(
`/courses/${courseId}/reviews`
);
},
async createReview(courseId: string, data: { rating: number; comment: string }) {
return api.post<ApiResponse<{ review: Review }>>(
`/courses/${courseId}/reviews`,
data
);
},
};export function useReviews(courseId: string) {
const [reviews, setReviews] = useState<Review[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
reviewsService.getReviews(courseId)
.then(res => setReviews(res.data.reviews))
.finally(() => setIsLoading(false));
}, [courseId]);
return { reviews, isLoading };
}export function ReviewsList({ courseId }: { courseId: string }) {
const { reviews, isLoading } = useReviews(courseId);
if (isLoading) return <Skeleton />;
return (
<div>
{reviews.map(review => (
<ReviewCard key={review._id} {...review} />
))}
</div>
);
}// β BAD: app/api/courses/route.ts
import { connectDB } from "@/lib/db";
import Course from "@/models/Course";
export async function GET() {
await connectDB();
const courses = await Course.find(); // NO! This is backend work
return Response.json(courses);
}// β
GOOD: lib/services/courses.service.ts
export const coursesService = {
async getCourses() {
return api.get("/courses"); // Calls http://localhost:5000/api/courses
},
};// β BAD: .env.local
NEXT_PUBLIC_JWT_SECRET=secret123 // Exposed to browser!
NEXT_PUBLIC_DATABASE_URL=mongodb://... // Exposed to browser!// β
GOOD: .env.local
NEXT_PUBLIC_API_URL=http://localhost:5000/api
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=my-cloud// β BAD
function EnrollButton({ courseId }) {
const handleEnroll = async () => {
// Don't calculate pricing, check eligibility, etc. here
const price = calculatePrice(course); // Business logic!
if (user.credits < price) { ... } // Business logic!
};
}// β
GOOD
function EnrollButton({ courseId }) {
const handleEnroll = async () => {
// Backend handles all business logic
const response = await coursesService.enrollCourse(courseId);
if (response.success) {
toast.success("Enrolled successfully!");
}
};
}- β Component rendering
- β User interactions
- β Form validation
- β API service calls (mocked)
- β Hook behavior
- β Database operations
- β Authentication token generation
- β Business logic validation
- β Server-side calculations
- Design the component β Focus on presentation
- Define the data contract β TypeScript types
- Create the service β API communication
- Build the hook β Data fetching logic
- Implement the component β Use the hook
- Style with Tailwind β Premium aesthetics
- Next.js 16 - App Router, Server Components
- TypeScript - Strict type safety
- Tailwind CSS - Utility-first styling
- Zod - Schema validation
- React Hook Form - Form management
- Sonner - Toast notifications
- Radix UI - Accessible components
- Backend API: See
Alpha-squad-back-end/README.md - API Endpoints: See
Alpha-squad-back-end/postman_collection.json - Component Library: See
src/components/ui/
Remember: This frontend is a presentation layer only. All data, logic, and security live in the backend. Keep it clean, keep it separated.