Instruction file imported from lisoingsem/cms-elysiajs-nextjs (
.cursor/rules/frontend.mdc). Copyright stays with the author.
Frontend (Next.js) Type Rules
Type Definition Location
All API types MUST be defined in:
types/api.ts- Main type definitions- Or module-specific type files if needed
Type Import Pattern
// ✅ CORRECT: Import from types/api.ts
import type { User, Course, Enrollment } from '@/types/api'
// ❌ WRONG: Don't define types inline
const user: { id: number; name: string } = ... // Wrong!
API Service Type Pattern
Service Function Pattern
// services/users.ts
import type { User, CreateUserRequest, UpdateUserRequest } from '@/types/api'
import apiClient from '@/lib/api'
export const users = {
async getAll(): Promise<User[]> {
const response = await apiClient.get<User[]>('/users')
return response.data
},
async getById(id: number): Promise<User> {
const response = await apiClient.get<User>(`/users/${id}`)
return response.data
},
async create(data: CreateUserRequest): Promise<User> {
const response = await apiClient.post<User>('/users', data)
return response.data
},
async update(id: number, data: UpdateUserRequest): Promise<User> {
const response = await apiClient.put<User>(`/users/${id}`, data)
return response.data
},
}
React Hook Type Pattern
Custom Hook Pattern
// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import type { User, CreateUserRequest, UpdateUserRequest } from '@/types/api'
import { users } from '@/services/users'
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => users.getAll(),
})
}
export function useUser(id: number) {
return useQuery({
queryKey: ['users', id],
queryFn: () => users.getById(id),
enabled: !!id,
})
}
export function useCreateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: CreateUserRequest) => users.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
}
Component Type Pattern
Component Props Pattern
// ✅ CORRECT: Explicit prop types
interface UserCardProps {
user: User
onEdit?: (user: User) => void
onDelete?: (id: number) => void
}
export function UserCard({ user, onEdit, onDelete }: UserCardProps) {
// Component implementation
}
// ❌ WRONG: Using any or implicit types
export function UserCard({ user, onEdit, onDelete }: any) { // Wrong!
// ...
}
Form Type Pattern
React Hook Form Pattern
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import type { CreateUserRequest } from '@/types/api'
// Zod schema matching backend TypeBox
const createUserSchema = z.object({
username: z.string().min(3).max(50),
email: z.string().email(),
name: z.string().min(1).max(100),
})
type CreateUserForm = z.infer<typeof createUserSchema>
export function CreateUserForm() {
const form = useForm<CreateUserForm>({
resolver: zodResolver(createUserSchema),
})
const createUser = useCreateUser()
const onSubmit = async (data: CreateUserForm) => {
await createUser.mutateAsync(data)
}
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* Form fields */}
</form>
)
}
Error Handling Type Pattern
Error Handling Pattern
import type { AxiosErrorResponse } from '@/types/api'
import { toast } from 'sonner'
// ✅ CORRECT: Typed error handling
const { mutate, error } = useMutation({
mutationFn: (data: CreateUserRequest) => users.create(data),
onError: (error: AxiosErrorResponse) => {
const message =
error?.response?.data?.message ||
error?.response?.data?.error ||
'Failed to create user'
toast.error(message)
},
})
// ❌ WRONG: Using any or unknown without type guard
onError: (error: any) => { // Wrong!
toast.error(error.message)
}
Type Guards
Runtime Type Checking
// ✅ CORRECT: Type guard functions
export function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'username' in data &&
'email' in data &&
typeof (data as any).id === 'number' &&
typeof (data as any).username === 'string' &&
typeof (data as any).email === 'string'
)
}
// Usage
const response = await apiClient.get('/users/1')
if (isUser(response.data)) {
// response.data is now typed as User
console.log(response.data.username)
}
Enum Type Pattern
Enum Matching Backend
// ✅ CORRECT: Match backend enum exactly
export type CourseStatus = 'draft' | 'published' | 'archived'
export type UserRole = 'student' | 'instructor' | 'admin' | 'assistant' | 'moderator'
export type PaymentStatus = 'pending' | 'processing' | 'completed' | 'failed' | 'refunded'
// Usage
const status: CourseStatus = 'published' // Type-safe
Nullable vs Optional
Handling Null/Undefined
// ✅ CORRECT: Match backend nullable/optional
export interface Course {
id: number
title: string
description: string | null // Nullable from backend
maxStudents?: number // Optional from backend
}
// Usage
if (course.description !== null) {
// TypeScript knows description is string here
console.log(course.description.length)
}
Date Handling
Date String Pattern
// Backend returns dates as ISO strings
export interface User {
id: number
createdAt: string // ISO date string
updatedAt: string // ISO date string
}
// Convert when needed
import { format } from 'date-fns'
const formattedDate = format(new Date(user.createdAt), 'MMM dd, yyyy')
API Client Type Pattern
Typed API Client
// lib/api.ts
import axios from 'axios'
const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
})
// Typed GET
export async function get<T>(url: string): Promise<{ data: T }> {
const response = await apiClient.get<T>(url)
return { data: response.data }
}
// Typed POST
export async function post<T, D = unknown>(
url: string,
data?: D
): Promise<{ data: T }> {
const response = await apiClient.post<T>(url, data)
return { data: response.data }
}
Type Consistency Checklist
Before committing frontend code:
- All API types match backend TypeBox schemas exactly
- No
anytypes used - All component props are explicitly typed
- All form schemas match backend validation
- Error handling uses
AxiosErrorResponse - Enums match backend enums exactly
- Nullable fields use
| null - Optional fields use
?or| undefined - Date fields are typed as
string(ISO format) - All service functions have explicit return types
- All hooks have proper type parameters
- Type guards used for runtime type checking when needed
Common Type Patterns
Pagination
export interface PaginatedResponse<T> {
data: T[]
total: number
page: number
limit: number
}
export interface PaginationParams {
page?: number
limit?: number
}
Filters
export interface CourseFilters {
status?: CourseStatus
instructorId?: number
categoryId?: number
search?: string
}
List Response
// Backend returns array
const courses: Course[] = await courses.getAll()
// Backend returns paginated
const response: PaginatedResponse<Course> = await courses.getPaginated(params)
Type Safety Best Practices
- Always import types - Don't redefine types
- Use type assertions sparingly - Prefer type guards
- Validate API responses - Use type guards for external data
- Match backend exactly - Frontend types must mirror backend
- Document type changes - Update types when backend changes
- Use strict TypeScript - Enable all strict checks
- Type all functions - Explicit return types and parameters
- Avoid
ascasts - Use proper type guards instead