| name | phase-6-ui-integration |
| classification | capability |
| classification-reason | Pattern guidance may overlap with model's built-in knowledge as it improves |
| deprecation-risk | medium |
| effort | medium |
| description | Implement frontend UI and integrate with backend APIs — state management and API clients.
Triggers: UI integration, frontend-backend, API client
default: bkit:pipeline-guide
frontend: bkit:frontend-architect
|
| allowed-tools | ["Read","Write","Edit","Glob","Grep","Bash"] |
| user-invocable | false |
| next-skill | phase-7-seo-security |
| pdca-phase | do |
| task-template | [Phase-6] {feature} |
Phase 6: UI Implementation + API Integration
Actual UI implementation and API integration
Purpose
Implement actual screens using design system components and integrate with APIs.
What to Do in This Phase
- Page Implementation: Develop each screen
- State Management: Handle client state
- API Integration: Call backend APIs
- Error Handling: Handle loading and error states
Deliverables
src/
├── pages/ # Page components
│ ├── index.tsx
│ ├── login.tsx
│ └── ...
├── features/ # Feature-specific components
│ ├── auth/
│ ├── product/
│ └── ...
└── hooks/ # API call hooks
├── useAuth.ts
└── useProducts.ts
docs/03-analysis/
└── ui-qa.md # QA results
PDCA Application
- Plan: Define screens/features to implement
- Design: Component structure, state management design
- Do: UI implementation + API integration
- Check: Zero Script QA
- Act: Fix bugs and proceed to Phase 7
Level-wise Application
| Level | Application Method |
|---|
| Starter | Static UI only (no API integration) |
| Dynamic | Full integration |
| Enterprise | Full integration + optimization |
API Client Architecture
Why is a Centralized API Client Needed?
| Problem (Scattered API Calls) | Solution (Centralized Client) |
|---|
| Duplicate error handling logic | Common error handler |
| Distributed auth token handling | Automatic token injection |
| Inconsistent response formats | Standardized response types |
| Multiple changes when endpoint changes | Single point of management |
| Difficult testing/mocking | Easy mock replacement |
3-Layer API Client Structure
┌─────────────────────────────────────────────────────────┐
│ UI Components │
│ (pages, features, hooks) │
├─────────────────────────────────────────────────────────┤
│ Service Layer │
│ (Domain-specific API call functions) │
│ authService, productService, orderService, ... │
├─────────────────────────────────────────────────────────┤
│ API Client Layer │
│ (Common settings, interceptors, error handling) │
│ apiClient (axios/fetch wrapper) │
└─────────────────────────────────────────────────────────┘
Folder Structure
src/
├── lib/
│ └── api/
│ ├── client.ts # API client (axios/fetch wrapper)
│ ├── interceptors.ts # Request/response interceptors
│ └── error-handler.ts # Error handling logic
├── services/
│ ├── auth.service.ts # Auth-related APIs
│ ├── product.service.ts # Product-related APIs
│ └── order.service.ts # Order-related APIs
├── types/
│ ├── api.types.ts # Common API types
│ ├── auth.types.ts # Auth domain types
│ └── product.types.ts # Product domain types
└── hooks/
├── useAuth.ts # Hooks using Service
└── useProducts.ts
API Client Implementation
1. Basic API Client (lib/api/client.ts)
import { ApiError, ApiResponse } from '@/types/api.types';
const BASE_URL = process.env.NEXT_PUBLIC_API_URL || '/api';
interface RequestConfig extends RequestInit {
params?: Record<string, string>;
}
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
private async request<T>(
endpoint: string,
config: RequestConfig = {}
): Promise<ApiResponse<T>> {
const { params, ...init } = config;
const url = new URL(`${this.baseUrl}${endpoint}`);
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
const headers = new Headers(init.headers);
if (!headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
const token = this.getAuthToken();
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
try {
const response = await fetch(url.toString(), {
...init,
headers,
});
return this.handleResponse<T>(response);
} catch (error) {
throw this.handleNetworkError(error);
}
}
private async handleResponse<T>(response: Response): Promise<ApiResponse<T>> {
const data = await response.json();
if (!response.ok) {
throw new ApiError(
data.error?.code || 'UNKNOWN_ERROR',
data.error?.message || 'An error occurred',
response.status,
data.error?.details
);
}
return data as ApiResponse<T>;
}
private handleNetworkError(error: unknown): ApiError {
if (error instanceof TypeError && error.message === 'Failed to fetch') {
return new ApiError('NETWORK_ERROR', 'Please check your network connection.', 0);
}
return new ApiError('UNKNOWN_ERROR', 'An unknown error occurred.', 0);
}
private getAuthToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('auth_token');
}
get<T>(endpoint: string, params?: Record<string, string>) {
return this.request<T>(endpoint, { method: 'GET', params });
}
post<T>(endpoint: string, body?: unknown) {
return this.request<T>(endpoint, {
method: 'POST',
body: JSON.stringify(body),
});
}
put<T>(endpoint: string, body?: unknown) {
return this.request<T>(endpoint, {
method: 'PUT',
body: JSON.stringify(body),
});
}
patch<T>(endpoint: string, body?: unknown) {
return this.request<T>(endpoint, {
method: 'PATCH',
body: JSON.stringify(body),
});
}
delete<T>(endpoint: string) {
return this.request<T>(endpoint, { method: 'DELETE' });
}
}
export const apiClient = new ApiClient(BASE_URL);
2. Common Type Definitions (types/api.types.ts)
export interface ApiResponse<T> {
data: T;
meta?: {
timestamp: string;
requestId?: string;
};
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
export interface ApiErrorResponse {
error: {
code: string;
message: string;
details?: Array<{
field: string;
message: string;
}>;
};
}
export class ApiError extends Error {
constructor(
public code: string,
message: string,
public status: number,
public details?: Array<{ field: string; message: string }>
) {
super(message);
this.name = 'ApiError';
}
isValidationError(): boolean {
return this.code === 'VALIDATION_ERROR' && !!this.details;
}
isAuthError(): boolean {
return this.status === 401 || this.code === 'UNAUTHORIZED';
}
isForbiddenError(): boolean {
return this.status === 403 || this.code === 'FORBIDDEN';
}
isNotFoundError(): boolean {
return this.status === 404 || this.code === 'NOT_FOUND';
}
}
export const ERROR_CODES = {
VALIDATION_ERROR: 'VALIDATION_ERROR',
UNAUTHORIZED: 'UNAUTHORIZED',
FORBIDDEN: 'FORBIDDEN',
NOT_FOUND: 'NOT_FOUND',
CONFLICT: 'CONFLICT',
INTERNAL_ERROR: 'INTERNAL_ERROR',
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
NETWORK_ERROR: 'NETWORK_ERROR',
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
} as const;
export type ErrorCode = typeof ERROR_CODES[keyof typeof ERROR_CODES];
Service Layer Pattern
Domain-specific Service Separation
import { apiClient } from '@/lib/api/client';
import { User, LoginRequest, LoginResponse, SignupRequest } from '@/types/auth.types';
export const authService = {
login(credentials: LoginRequest) {
return apiClient.post<LoginResponse>('/auth/login', credentials);
},
signup(data: SignupRequest) {
return apiClient.post<User>('/auth/signup', data);
},
logout() {
return apiClient.post<void>('/auth/logout');
},
getMe() {
return apiClient.get<User>('/auth/me');
},
refreshToken() {
return apiClient.post<LoginResponse>('/auth/refresh');
},
};
import { apiClient } from '@/lib/api/client';
import { Product, ProductFilter, CreateProductRequest } from '@/types/product.types';
import { PaginatedResponse } from '@/types/api.types';
export const productService = {
getList(filter?: ProductFilter) {
const params = filter ? {
page: String(filter.page || 1),
limit: String(filter.limit || 20),
...(filter.category && { category: filter.category }),
...(filter.search && { search: filter.search }),
} : undefined;
return apiClient.get<PaginatedResponse<Product>>('/products', params);
},
getById(id: string) {
return apiClient.get<Product>(`/products/${id}`);
},
create(data: CreateProductRequest) {
return apiClient.post<Product>('/products', data);
},
update(id: string, data: Partial<CreateProductRequest>) {
return apiClient.patch<Product>(`/products/${id}`, data);
},
delete(id: string) {
return apiClient.delete<void>(`/products/${id}`);
},
};
Error Handling Pattern
Global Error Handler
import { ApiError, ERROR_CODES } from '@/types/api.types';
import { toast } from 'sonner';
interface ErrorHandlerOptions {
showToast?: boolean;
redirectOnAuth?: boolean;
customMessages?: Record<string, string>;
}
export function handleApiError(
error: unknown,
options: ErrorHandlerOptions = {}
): void {
const { showToast = true, redirectOnAuth = true, customMessages = {} } = options;
if (!(error instanceof ApiError)) {
console.error('Unexpected error:', error);
if (showToast) {
toast.error('An unknown error occurred.');
}
return;
}
const message = customMessages[error.code] || error.message;