Code Standards — Frontend & Workflow
| Project | PTX Channel Manager (ptx-cm) |
| Last Updated | 2026-02-20 |
| See also | code-standards.md (principles & naming) |
1. Frontend (Next.js/React) Standards
Component File Structure
typescript
// ✓ Good: Typed props, separated concerns, named export
'use client';
import { useForm } from 'react-hook-form';
interface PropertyFormProps {
onSubmit: (data: CreatePropertyDto) => Promise<void>;
loading?: boolean;
}
export function PropertyForm({ onSubmit, loading }: PropertyFormProps) {
const { register, handleSubmit, formState: { errors } } = useForm<CreatePropertyDto>();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
<button type="submit" disabled={loading}>Create</button>
</form>
);
}
// ✗ Bad: All logic inlined, no types, default export only
export default function Page() {
const [properties, setProperties] = useState([]);
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({});
// ... 300 lines of inline logic and JSX
}Data Fetching: SWR via useApiGet
typescript
// ✓ Good: SWR hook with dedup + loading state
import { useApiGet } from '@/lib/use-api';
export function PropertyList() {
const { data: properties, isLoading } = useApiGet<Property[]>('properties', {
dedupingInterval: 30_000,
});
if (isLoading) return <LoadingSkeleton />;
if (!properties?.length) return <EmptyState />;
return <DataTable data={properties} columns={columns} />;
}
// ✗ Bad: Inline fetch in useEffect, no dedup
export function PropertyList() {
const [properties, setProperties] = useState([]);
useEffect(() => {
fetch('/api/v1/properties')
.then(r => r.json())
.then(setProperties); // ❌ No loading state, no dedup, no error handling
}, []);
}API Client Pattern
typescript
// ✓ Good: Centralized apiFetch with 401 auto-retry
import { apiFetch } from '@/lib/api-client';
export const propertyApi = {
list: (params?: ListPropertiesDto) => apiFetch<PaginatedResponse<Property>>('properties', { params }),
create: (dto: CreatePropertyDto) => apiFetch<Property>('properties', { method: 'POST', body: dto }),
get: (id: string) => apiFetch<Property>(`properties/${id}`),
};
// ✗ Bad: Inline fetch calls duplicated across components
export function PropertyList() {
useEffect(() => {
fetch('/api/v1/properties') // ❌ Repeated in multiple places, no error handling
.then(r => r.json())
.then(setProperties);
}, []);
}Context Provider Usage
typescript
// ✓ Good: Use typed context hooks
import { useAuth } from '@/lib/auth-context';
import { useCountry } from '@/lib/country-context';
export function BookingList() {
const { user } = useAuth();
const { country } = useCountry();
const { data } = useApiGet<Booking[]>(`bookings?country=${country}`);
// ...
}
// ✗ Bad: Reading context directly via useContext
import { AuthContext } from '@/lib/auth-context';
export function BookingList() {
const ctx = useContext(AuthContext); // ❌ No null check, not typed
const user = ctx?.user;
}Permission Checks
typescript
// ✓ Good: hasPermission helper from types package
import { hasPermission, Permission } from '@ptx-cm/types';
export function PropertyActions({ property }: { property: Property }) {
const { user } = useAuth();
const canEdit = hasPermission(user.permissions, 'PROPERTIES', Permission.EDIT);
const canDelete = hasPermission(user.permissions, 'PROPERTIES', Permission.DELETE);
return (
<>
{canEdit && <button onClick={onEdit}>Edit</button>}
{canDelete && <button onClick={onDelete}>Delete</button>}
</>
);
}
// ✗ Bad: Hardcoded role checks
export function PropertyActions() {
const { user } = useAuth();
if (user.roleName === 'manager') { // ❌ Brittle, bypasses permission bitmask
return <button>Edit</button>;
}
}2. Git Commit Standards
Conventional Commits
<type>(<scope>): <subject>
<body>
<footer>Types:
| Type | Use for |
|---|---|
feat | New feature |
fix | Bug fix |
docs | Documentation only |
style | Formatting, no logic change |
refactor | Code restructure, no feature change |
test | Add or update tests |
chore | Build, CI, dependency updates |
Examples:
bash
# ✓ Good
git commit -m "feat(auth): add refresh token revocation on logout"
git commit -m "fix(properties): prevent cross-country property access"
git commit -m "docs: update API_SPEC with country scoping details"
git commit -m "refactor(sync-engine): extract availability calc into separate service"
# ✗ Bad
git commit -m "fix stuff"
git commit -m "Update code"
git commit -m "wip"Pre-Push Checklist
bash
pnpm lint # No errors
pnpm test # All tests pass
pnpm build # No compile errorsNever commit:
.envfiles or API keysnode_modules/,dist/,.next/- Generated Prisma client files
3. Performance Guidelines
API Response Size
typescript
// ✓ Good: Select only fields needed for list view
const bookings = await this.prisma.booking.findMany({
select: {
id: true,
otaBookingId: true,
guestName: true,
checkIn: true,
checkOut: true,
status: true,
// Exclude: rawData, syncHistory (large fields for detail view only)
},
});
// ✗ Bad: Return all fields including large nested data
const bookings = await this.prisma.booking.findMany();
// Response includes rawData (full OTA response JSON), syncHistory arrays, etc.Always Paginate
typescript
// ✓ Good: Default 25, max 100
@Get()
async findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(25), ParseIntPipe) limit: number,
) {
// Cap at 100 to prevent abuse
const safeLimitimit = Math.min(limit, 100);
return this.service.findAll(page, safeLimit);
}Frontend: Avoid Unnecessary Re-renders
typescript
// ✓ Good: Stable callback references
const updateUser = useCallback((updated: Partial<User>) => {
setUser(prev => prev ? { ...prev, ...updated } : null);
}, []);
// ✓ Good: Memoize derived data
const overdueAlerts = useMemo(
() => alerts?.filter(a => !a.resolvedAt) ?? [],
[alerts],
);
// ✗ Bad: Inline functions recreated each render
<button onClick={() => updateUser({ locale: 'vi' })}>
Switch to Vietnamese
</button>
// ❌ New function reference every render causes child re-render4. Related Documentation
- code-standards.md — Principles, naming, file size rules
- code-standards-backend.md — NestJS, security, DB, testing
- system-architecture.md — Architecture diagrams
- UI_SPEC.md — UI design system and screen specs