This project uses openapi-typescript to automatically generate TypeScript types from the OpenAPI 3.0 specification. This ensures type safety and eliminates manual type definition maintenance.
Key Benefits:
- Single Source of Truth: API contract (OpenAPI spec) → TypeScript types
- Type Safety: Compile-time validation of API requests/responses
- Auto-sync: Types automatically update when API spec changes
- Zero Manual Maintenance: No need to manually write or update DTO types
docs/
└── openapi.json # OpenAPI 3.0 spec (source of truth)
src/lib/types/
├── api.d.ts # Auto-generated types (DO NOT EDIT)
└── index.ts # Single source of truth for imports
Important:
api.d.tsis auto-generated - never edit it manuallyindex.tsis the only file you should import from
npm install -D openapi-typescript{
"scripts": {
"generate:types": "openapi-typescript docs/openapi.json -o src/lib/types/api.d.ts"
}
}# After updating docs/openapi.json
npm run generate:typesThis command:
- Reads
docs/openapi.json(OpenAPI 3.0 spec) - Generates
src/lib/types/api.d.tswith:paths- All API endpointscomponents- Schema definitions (DTOs)operations- Request/response types for each endpoint
- Types are ready to use via
@/lib/types
RULE: ALWAYS import from @/lib/types. NEVER import from @/lib/types/api
// API modules
import type {
LoginRequestDto,
LoginResponseDto,
FindNoticesParams,
} from '@/lib/types';
// Store modules
import type {
NoticeDetailResponseDto,
CreateNoticeDto,
} from '@/lib/types';
// Components
import type {
UserRole,
JoinStatus,
ApartmentResponseDto,
} from '@/lib/types';// NEVER import directly from api.d.ts
import type { components } from '@/lib/types/api'; // ❌ WRONG
import type { operations } from '@/lib/types/api'; // ❌ WRONG
// NEVER use operations/components directly
import type { operations } from '@/lib/types';
type Params = operations['NoticesController_findAll']['parameters']['query']; // ❌ WRONG
// NEVER manually access nested paths
type LoginDto = components['schemas']['LoginRequestDto']; // ❌ WRONG- Encapsulation:
api.d.tsis an internal implementation detail - Single Import Path: All code uses the same import source (
@/lib/types) - Maintainability: Type exports are centrally managed in
index.ts - Readability: Clean, short type names instead of nested paths
- Flexibility: Can extend or alias types without affecting consumers
All request/response DTOs are exported from @/lib/types:
import type {
SignupUserRequestDto,
SignupUserResponseDto,
SignupAdminRequestDto,
LoginRequestDto,
LoginResponseDto,
UpdateStatusDto,
ChangePasswordDto,
} from '@/lib/types';import type {
ApartmentResponseDto,
ApartmentListResponseDto,
ApartmentSummaryDto,
ResidentResponseDto,
CreateOneResidentDto,
UpdateResidentDto,
} from '@/lib/types';import type {
NoticeDetailResponseDto,
CreateNoticeDto,
UpdateNoticeDto,
PollFindOneResponseDto,
CreatePollDto,
ComplaintsResponseDto,
CreateComplaintDto,
} from '@/lib/types';import type {
CommentResponseDto,
CreateCommentDto,
NotificationDto,
} from '@/lib/types';import type {
MessageResponseDto,
CreateResponseDto,
DeleteResponseDto,
BulkOperationResponseDto,
} from '@/lib/types';Query parameters are extracted from operations and exported:
import type {
FindResidentsParams, // Resident list filters
FindApartmentsParams, // Apartment search
FindNoticesParams, // Notice filters
FindPollsParams, // Poll filters
FindComplaintsParams, // Complaint filters
GetEventsParams, // Event calendar params
} from '@/lib/types';Example Usage:
import { apiClient } from '@/lib/api/client';
import type { FindNoticesParams, NoticesListWrapperDto } from '@/lib/types';
export const getNotices = async (params: FindNoticesParams) => {
const response = await apiClient.get<NoticesListWrapperDto>('/notices', { params });
return response.data;
};Status and role enums extracted from DTOs:
import type {
UserRole, // "SUPER_ADMIN" | "ADMIN" | "USER"
JoinStatus, // "PENDING" | "APPROVED" | "REJECTED" | "NEED_UPDATE"
ApartmentStatus, // "PENDING" | "APPROVED" | "REJECTED"
ResidenceStatus, // "RESIDENCE" | "NO_RESIDENCE"
NoticeCategory, // "MAINTENANCE" | "EMERGENCY" | "COMMUNITY" | ...
ComplaintStatus, // "PENDING" | "IN_PROGRESS" | "RESOLVED" | "REJECTED"
PollStatus, // "PENDING" | "IN_PROGRESS" | "CLOSED"
NotificationType, // "GENERAL" | "SIGNUP_REQ" | "COMPLAINT_REQ" | ...
EventType, // "NOTICE" | "POLL"
} from '@/lib/types';import type {
PaginatedResponse, // Generic paginated wrapper
ErrorResponse, // Standard error response
} from '@/lib/types';File: src/lib/api/notices.ts
import { apiClient } from './client';
import type {
NoticeDetailResponseDto,
NoticesListWrapperDto,
CreateNoticeDto,
UpdateNoticeDto,
FindNoticesParams,
MessageResponseDto,
} from '@/lib/types';
// List with filters
export const getNotices = async (params?: FindNoticesParams) => {
const response = await apiClient.get<NoticesListWrapperDto>('/notices', { params });
return response.data;
};
// Get single notice
export const getNotice = async (noticeId: string) => {
const response = await apiClient.get<NoticeDetailResponseDto>(`/notices/${noticeId}`);
return response.data;
};
// Create notice
export const createNotice = async (data: CreateNoticeDto) => {
const response = await apiClient.post<{ id: string }>('/notices', data);
return response.data;
};
// Update notice
export const updateNotice = async (noticeId: string, data: UpdateNoticeDto) => {
const response = await apiClient.patch<UpdateNoticeDto>(`/notices/${noticeId}`, data);
return response.data;
};
// Delete notice
export const deleteNotice = async (noticeId: string) => {
const response = await apiClient.delete<MessageResponseDto>(`/notices/${noticeId}`);
return response.data;
};Key Points:
- ✅ All types imported from
@/lib/types - ✅ Request/response types match OpenAPI spec exactly
- ✅ TypeScript validates params, body, and response shapes
- ✅ No manual type definitions needed
# Workflow when backend updates API
git pull # Get latest openapi.json
npm run generate:types # Regenerate types
npm run build # Check for type errorsIf you need a type that's not yet exported:
// src/lib/types/index.ts
// Add the export
export type NewTypeDto = components['schemas']['NewTypeDto'];
// Or extract from operations
export type NewQueryParams = operations['ControllerName_methodName']['parameters']['query'];Don't create manual types! Always source from api.d.ts.
Some DTOs have nested types (e.g., SignupUserRequestDto$Apartment). These are also exported:
import type {
SignupUserRequestDto,
SignupUserApartment, // Alias for SignupUserRequestDto$Apartment
} from '@/lib/types';// ❌ BAD - No type safety
const response = await apiClient.get('/notices');
const notices = response.data; // Type: any
// ✅ GOOD - Full type safety
import type { NoticesListWrapperDto } from '@/lib/types';
const response = await apiClient.get<NoticesListWrapperDto>('/notices');
const notices = response.data; // Type: NoticesListWrapperDtoimport type { ErrorResponse } from '@/lib/types';
try {
await createNotice(data);
} catch (error: any) {
const apiError = error.response?.data as ErrorResponse;
console.error(apiError.message, apiError.statusCode);
}Problem: Cannot find name 'SomeDto'
Solution:
- Check if it exists in
api.d.tsundercomponents.schemas - If yes, add export to
src/lib/types/index.ts - If no, update
docs/openapi.jsonand runnpm run generate:types
Problem: API response doesn't match type
Solution:
- Check if
docs/openapi.jsonis up to date with backend - Run
npm run generate:types - If still failing, backend API may have changed without updating spec
Problem: Module '"@/lib/types"' has no exported member 'SomeType'
Solution:
- Check if you're importing from
@/lib/types(not@/lib/types/api) - Check if the type is exported in
src/lib/types/index.ts - Check spelling - type names are case-sensitive
// src/lib/types/api.d.ts (auto-generated)
export interface paths {
"/api/notices": {
get: operations["NoticesController_findAll"];
post: operations["NoticesController_create"];
};
// ... all endpoints
}
export interface components {
schemas: {
NoticeDetailResponseDto: {
id: string;
title: string;
content: string;
// ... all fields
};
// ... all DTOs
};
}
export interface operations {
NoticesController_findAll: {
parameters: {
query?: {
page?: number;
limit?: number;
category?: string;
search?: string;
};
};
responses: {
200: {
content: {
"application/json": components["schemas"]["NoticesListWrapperDto"];
};
};
};
};
// ... all operations
}// src/lib/types/index.ts
import type { components, operations } from './api';
// Re-export for advanced usage
export type { components, operations };
// Extract and export schema types
export type NoticeDetailResponseDto = components['schemas']['NoticeDetailResponseDto'];
// Extract and export query params from operations
export type FindNoticesParams = operations['NoticesController_findAll']['parameters']['query'];
// Extract enum types from DTOs
export type NoticeCategory = NoticeDetailResponseDto['category'];This pattern:
- ✅ Centralizes all type exports
- ✅ Provides clean, short names
- ✅ Maintains single source of truth
- ✅ Allows type extensions without affecting consumers
| Do | Don't |
|---|---|
✅ Import from @/lib/types |
❌ Import from @/lib/types/api |
✅ Run generate:types after API changes |
❌ Manually edit api.d.ts |
✅ Add exports to index.ts if needed |
❌ Create manual type definitions |
| ✅ Use generated types in API modules | ❌ Use any or untyped responses |
✅ Keep openapi.json up to date |
❌ Let types drift from backend |
Remember: The OpenAPI spec is the single source of truth. Let TypeScript guide you!