🔧 REST API 서버 만들기
📖 정의
REST API는 HTTP 프로토콜을 사용하여 클라이언트와 서버가 데이터를 주고받는 웹 서비스 인터페이스입니다. Express.js는 Node.js 기반의 경량 웹 프레임워크로, REST API를 빠르고 쉽게 구축할 수 있게 해줍니다. CRUD 작업(생성, 조회, 수정, 삭제)을 HTTP 메서드(GET, POST, PUT, DELETE)로 구현합니다.
🎯 비유로 이해하기
레스토랑 시스템
REST API를 레스토랑에 비유하면:
레스토랑 = API 서버
├─ 웨이터 = API 엔드포인트
├─ 메뉴판 = API 문서
├─ 주문서 = HTTP 요청
└─ 요리 = HTTP 응답
손님(클라이언트)의 주문:
GET /menu → 메뉴판 보여주세요 (조회)
POST /orders → 주문할게요 (생성)
GET /orders/123 → 제 주문 어디까지 왔나요? (조회)
PUT /orders/123 → 주문 변경할게요 (수정)
DELETE /orders/123 → 주문 취소할게요 (삭제)
웨이터는 정해진 규칙대로만 응답!
도서관 시스템
도서관 = REST API
├─ 사서 = 서버
├─ 도서 목록 = 데이터베이스
└─ 대출증 = 인증 토큰
REST 원칙:
1. 무상태(Stateless)
└─ 사서는 이전 대화를 기억 안 함
└─ 매번 대출증(토큰)을 보여줘야 함
2. 자원 기반(Resource-based)
└─ /books/123 = 도서 번호 123번
└─ /users/456 = 회원 번호 456번
3. HTTP 메서드로 행동 표현
└─ GET = 책 찾기
└─ POST = 신규 등록
└─ PUT = 정보 수정
└─ DELETE = 폐기
⚙️ 작동 원리
1. HTTP 메서드와 CRUD
CRUD 작업 → HTTP 메서드
CREATE (생성)
└─ POST /api/users
Body: { "name": "김철수", "email": "kim@example.com" }
READ (조회)
├─ GET /api/users (전체 목록)
└─ GET /api/users/123 (특정 항목)
UPDATE (수정)
├─ PUT /api/users/123 (전체 수정)
└─ PATCH /api/users/123 (부분 수정)
Body: { "name": "김영희" }
DELETE (삭제)
└─ DELETE /api/users/123
2. 요청-응답 흐름
클라이언트 서버
| |
| GET /api/users/123 |
|--------------------------->|
| | 1. 라우터 매칭
| | 2. 미들웨어 실행
| | 3. 컨트롤러 로직
| | 4. DB 조회
| | 5. 응답 생성
| |
| 200 OK |
| { id: 123, name: "김철수" }|
|<---------------------------|
| |
3. REST API 설계 규칙
URL 구조:
✅ /api/users (복수형 명사)
✅ /api/users/123 (ID로 특정)
✅ /api/users/123/posts (관계)
❌ /api/getUser (동사 사용 X)
❌ /api/user/delete (동사 사용 X)
HTTP 상태 코드:
200 OK - 성공
201 Created - 생성 성공
204 No Content - 삭제 성공 (본문 없음)
400 Bad Request - 잘못된 요청
401 Unauthorized - 인증 필요
403 Forbidden - 권한 없음
404 Not Found - 찾을 수 없음
500 Internal Server Error - 서버 오류
💡 실제 예시
환경 설정
# 1. Node.js 설치 확인
node --version
npm --version
# 2. 프로젝트 생성
mkdir todo-api
cd todo-api
# 3. package.json 초기화
npm init -y
# 4. 필요한 패키지 설치
npm install express cors dotenv
npm install --save-dev nodemon
# express: 웹 프레임워크
# cors: 크로스 오리진 요청 처리
# dotenv: 환경변수 관리
# nodemon: 자동 재시작 (개발용)
# 5. package.json 수정
cat > package.json << 'EOF'
{
"name": "todo-api",
"version": "1.0.0",
"description": "REST API for Todo App",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"keywords": ["rest", "api", "express", "todo"],
"author": "Your Name",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}
EOF
Step 1: 기본 서버 구축
// server.js
const express = require('express');
const cors = require('cors');
require('dotenv').config();
// Express 앱 생성
const app = express();
const PORT = process.env.PORT || 3000;
// 미들웨어 설정
app.use(cors()); // CORS 허용
app.use(express.json()); // JSON 파싱
app.use(express.urlencoded({ extended: true })); // URL-encoded 파싱
// 요청 로깅 미들웨어 (직접 구현)
app.use((req, res, next) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${req.method} ${req.path}`);
next(); // 다음 미들웨어로
});
// 루트 엔드포인트
app.get('/', (req, res) => {
res.json({
message: 'Welcome to Todo API!',
version: '1.0.0',
endpoints: {
todos: '/api/todos',
health: '/health'
}
});
});
// Health check 엔드포인트
app.get('/health', (req, res) => {
res.status(200).json({
status: 'OK',
timestamp: new Date().toISOString()
});
});
// 404 핸들러
app.use((req, res) => {
res.status(404).json({
error: 'Not Found',
message: `Route ${req.method} ${req.path} does not exist.`
});
});
// 에러 핸들러
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: 'Internal Server Error',
message: err.message
});
});
// 서버 시작
app.listen(PORT, () => {
console.log(`🚀 Server is running on http://localhost:${PORT}`);
console.log(`📝 Environment: ${process.env.NODE_ENV || 'development'}`);
});
# .env 파일 생성
cat > .env << EOF
PORT=3000
NODE_ENV=development
EOF
# .gitignore 생성
cat > .gitignore << EOF
node_modules/
.env
.DS_Store
*.log
EOF
# 서버 실행
npm run dev
# 출력:
# 🚀 Server is running on http://localhost:3000
# 📝 Environment: development
# 테스트
curl http://localhost:3000
# {"message":"Welcome to Todo API!",...}
curl http://localhost:3000/health
# {"status":"OK","timestamp":"2024-01-15T..."}
Step 2: Todo CRUD API 구현 (메모리 저장)
// routes/todos.js
const express = require('express');
const router = express.Router();
// 임시 데이터 저장소 (메모리)
let todos = [
{ id: 1, title: 'Learn Node.js', completed: false, createdAt: new Date() },
{ id: 2, title: 'Build REST API', completed: false, createdAt: new Date() },
{ id: 3, title: 'Deploy to Heroku', completed: false, createdAt: new Date() }
];
let nextId = 4; // 다음 ID
// GET /api/todos - 전체 목록 조회
router.get('/', (req, res) => {
// 쿼리 파라미터로 필터링
const { completed } = req.query;
let filteredTodos = todos;
// completed 필터 적용
if (completed !== undefined) {
const isCompleted = completed === 'true';
filteredTodos = todos.filter(todo => todo.completed === isCompleted);
}
res.json({
success: true,
count: filteredTodos.length,
data: filteredTodos
});
});
// GET /api/todos/:id - 특정 항목 조회
router.get('/:id', (req, res) => {
const id = parseInt(req.params.id);
const todo = todos.find(t => t.id === id);
if (!todo) {
return res.status(404).json({
success: false,
error: 'Todo not found'
});
}
res.json({
success: true,
data: todo
});
});
// POST /api/todos - 새 항목 생성
router.post('/', (req, res) => {
// 유효성 검사
const { title } = req.body;
if (!title || title.trim() === '') {
return res.status(400).json({
success: false,
error: 'Title is required'
});
}
// 새 Todo 생성
const newTodo = {
id: nextId++,
title: title.trim(),
completed: false,
createdAt: new Date()
};
todos.push(newTodo);
// 201 Created 상태 코드
res.status(201).json({
success: true,
data: newTodo
});
});
// PUT /api/todos/:id - 항목 수정 (전체)
router.put('/:id', (req, res) => {
const id = parseInt(req.params.id);
const todoIndex = todos.findIndex(t => t.id === id);
if (todoIndex === -1) {
return res.status(404).json({
success: false,
error: 'Todo not found'
});
}
const { title, completed } = req.body;
// 유효성 검사
if (!title || title.trim() === '') {
return res.status(400).json({
success: false,
error: 'Title is required'
});
}
// 전체 교체
todos[todoIndex] = {
...todos[todoIndex],
title: title.trim(),
completed: completed !== undefined ? completed : todos[todoIndex].completed,
updatedAt: new Date()
};
res.json({
success: true,
data: todos[todoIndex]
});
});
// PATCH /api/todos/:id - 항목 수정 (부분)
router.patch('/:id', (req, res) => {
const id = parseInt(req.params.id);
const todoIndex = todos.findIndex(t => t.id === id);
if (todoIndex === -1) {
return res.status(404).json({
success: false,
error: 'Todo not found'
});
}
const { title, completed } = req.body;
// 제공된 필드만 업데이트
if (title !== undefined) {
if (title.trim() === '') {
return res.status(400).json({
success: false,
error: 'Title cannot be empty'
});
}
todos[todoIndex].title = title.trim();
}
if (completed !== undefined) {
todos[todoIndex].completed = completed;
}
todos[todoIndex].updatedAt = new Date();
res.json({
success: true,
data: todos[todoIndex]
});
});
// DELETE /api/todos/:id - 항목 삭제
router.delete('/:id', (req, res) => {
const id = parseInt(req.params.id);
const todoIndex = todos.findIndex(t => t.id === id);
if (todoIndex === -1) {
return res.status(404).json({
success: false,
error: 'Todo not found'
});
}
const deletedTodo = todos.splice(todoIndex, 1)[0];
// 204 No Content 또는 200 OK
res.json({
success: true,
message: 'Todo deleted successfully',
data: deletedTodo
});
});
// DELETE /api/todos - 전체 삭제
router.delete('/', (req, res) => {
const count = todos.length;
todos = [];
nextId = 1;
res.json({
success: true,
message: `${count} todos deleted successfully`
});
});
module.exports = router;
// server.js 업데이트 (라우터 추가)
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
// 미들웨어
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 로깅 미들웨어
app.use((req, res, next) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${req.method} ${req.path}`);
next();
});
// 루트
app.get('/', (req, res) => {
res.json({
message: 'Welcome to Todo API!',
version: '1.0.0',
endpoints: {
todos: '/api/todos',
health: '/health'
}
});
});
// Todo 라우터 연결
const todosRouter = require('./routes/todos');
app.use('/api/todos', todosRouter);
// Health check
app.get('/health', (req, res) => {
res.status(200).json({
status: 'OK',
timestamp: new Date().toISOString()
});
});
// 404 핸들러
app.use((req, res) => {
res.status(404).json({
error: 'Not Found',
message: `Route ${req.method} ${req.path} does not exist.`
});
});
// 에러 핸들러
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: 'Internal Server Error',
message: err.message
});
});
app.listen(PORT, () => {
console.log(`🚀 Server is running on http://localhost:${PORT}`);
});
module.exports = app; // 테스트용
Step 3: API 테스트
# 1. 전체 목록 조회
curl http://localhost:3000/api/todos
# 응답:
# {
# "success": true,
# "count": 3,
# "data": [
# { "id": 1, "title": "Learn Node.js", "completed": false, ... },
# { "id": 2, "title": "Build REST API", "completed": false, ... },
# { "id": 3, "title": "Deploy to Heroku", "completed": false, ... }
# ]
# }
# 2. 특정 항목 조회
curl http://localhost:3000/api/todos/1
# 3. 새 항목 생성
curl -X POST http://localhost:3000/api/todos \
-H "Content-Type: application/json" \
-d '{"title": "Write documentation"}'
# 응답:
# {
# "success": true,
# "data": {
# "id": 4,
# "title": "Write documentation",
# "completed": false,
# "createdAt": "2024-01-15T..."
# }
# }
# 4. 항목 수정 (부분)
curl -X PATCH http://localhost:3000/api/todos/1 \
-H "Content-Type: application/json" \
-d '{"completed": true}'
# 5. 항목 삭제
curl -X DELETE http://localhost:3000/api/todos/1
# 6. 필터링 (완료된 항목만)
curl http://localhost:3000/api/todos?completed=true
# 7. 필터링 (미완료 항목만)
curl http://localhost:3000/api/todos?completed=false