REST API
Representational State Transfer
HTTP 메서드(GET, POST, PUT, DELETE)를 사용하는 API 설계 스타일. 무상태성이 핵심 원칙.
Representational State Transfer
HTTP 메서드(GET, POST, PUT, DELETE)를 사용하는 API 설계 스타일. 무상태성이 핵심 원칙.
REST API(Representational State Transfer)는 2000년 Roy Fielding의 박사 논문에서 처음 제안된 아키텍처 스타일로, 웹 서비스 설계의 사실상 표준이 되었습니다. HTTP 프로토콜의 메서드(GET, POST, PUT, DELETE, PATCH)를 활용하여 리소스를 CRUD(Create, Read, Update, Delete) 방식으로 조작합니다. URL은 리소스를 명사형으로 표현하고, HTTP 메서드가 동작을 나타내는 것이 RESTful 설계의 핵심입니다.
REST의 6가지 제약 조건은 클라이언트-서버 분리, 무상태성(Stateless), 캐시 가능성, 계층화된 시스템, 코드 온 디맨드(선택), 통합 인터페이스입니다. 특히 무상태성은 각 요청이 독립적이며 서버가 클라이언트의 상태를 저장하지 않음을 의미합니다. 이로 인해 서버 확장이 용이하고 로드 밸런싱이 간단해집니다.
RESTful API 설계에서는 리소스 명명 규칙이 중요합니다. 복수형 명사(/users), 계층적 관계 표현(/users/1/posts), 쿼리 파라미터를 통한 필터링(/users?role=admin)이 권장됩니다. HTTP 상태 코드도 의미있게 사용해야 합니다. 200(성공), 201(생성됨), 400(잘못된 요청), 401(인증 필요), 404(없음), 500(서버 오류) 등이 있습니다.
현대 API 설계에서는 버전 관리(/api/v1/), 페이지네이션, HATEOAS(Hypermedia as the Engine of Application State), OpenAPI(Swagger) 문서화가 중요합니다. GraphQL, gRPC와 같은 대안이 있지만, REST는 간단함과 HTTP 표준 활용으로 여전히 가장 널리 사용됩니다. API Gateway, Rate Limiting, CORS 설정도 실무에서 필수적인 고려사항입니다.
// Express.js로 RESTful API 구현
import express from 'express';
const app = express();
app.use(express.json());
interface User {
id: number;
name: string;
email: string;
}
let users: User[] = [
{ id: 1, name: '홍길동', email: 'hong@example.com' },
{ id: 2, name: '김영희', email: 'kim@example.com' },
];
// GET /users - 전체 사용자 목록 조회
app.get('/api/v1/users', (req, res) => {
const { role, limit = 10, offset = 0 } = req.query;
let result = users;
// 페이지네이션 적용
result = result.slice(Number(offset), Number(offset) + Number(limit));
res.json({
data: result,
pagination: {
total: users.length,
limit: Number(limit),
offset: Number(offset),
}
});
});
// GET /users/:id - 특정 사용자 조회
app.get('/api/v1/users/:id', (req, res) => {
const user = users.find(u => u.id === Number(req.params.id));
if (!user) {
return res.status(404).json({
error: 'Not Found',
message: `User with id ${req.params.id} not found`
});
}
res.json({ data: user });
});
// POST /users - 새 사용자 생성
app.post('/api/v1/users', (req, res) => {
const { name, email } = req.body;
// 유효성 검사
if (!name || !email) {
return res.status(400).json({
error: 'Bad Request',
message: 'name and email are required'
});
}
const newUser: User = {
id: users.length + 1,
name,
email
};
users.push(newUser);
// 201 Created와 Location 헤더 반환
res.status(201)
.header('Location', `/api/v1/users/${newUser.id}`)
.json({ data: newUser });
});
// PUT /users/:id - 전체 업데이트 (Idempotent)
app.put('/api/v1/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === Number(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'Not Found' });
}
const { name, email } = req.body;
users[index] = { id: Number(req.params.id), name, email };
res.json({ data: users[index] });
});
// PATCH /users/:id - 부분 업데이트
app.patch('/api/v1/users/:id', (req, res) => {
const user = users.find(u => u.id === Number(req.params.id));
if (!user) {
return res.status(404).json({ error: 'Not Found' });
}
Object.assign(user, req.body);
res.json({ data: user });
});
// DELETE /users/:id - 삭제
app.delete('/api/v1/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === Number(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'Not Found' });
}
users.splice(index, 1);
// 204 No Content - 본문 없이 성공 응답
res.status(204).send();
});
// 에러 핸들링 미들웨어
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error(err.stack);
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong'
});
});
app.listen(3000, () => console.log('API server running on port 3000'));
// REST API 클라이언트 유틸리티
const API_BASE = 'https://api.example.com/v1';
async function apiClient<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const response = await fetch(`${API_BASE}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`,
...options.headers,
},
...options,
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || `HTTP ${response.status}`);
}
// 204 No Content 처리
if (response.status === 204) {
return null as T;
}
return response.json();
}
// CRUD 작업
const api = {
// Read
getUsers: () => apiClient<{ data: User[] }>('/users'),
getUser: (id: number) => apiClient<{ data: User }>(`/users/${id}`),
// Create
createUser: (data: Omit<User, 'id'>) =>
apiClient<{ data: User }>('/users', {
method: 'POST',
body: JSON.stringify(data),
}),
// Update
updateUser: (id: number, data: Partial<User>) =>
apiClient<{ data: User }>(`/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
}),
// Delete
deleteUser: (id: number) =>
apiClient<null>(`/users/${id}`, { method: 'DELETE' }),
};