728x90
반응형
1. REST API란
REST(Representational State Transfer)는 웹 서비스를 설계하기 위한 아키텍처 스타일입니다. HTTP 메서드(GET, POST, PUT, DELETE)를 사용하여 리소스를 조작하며, 상태를 저장하지 않는(Stateless) 특성을 가집니다.
1.1 REST 설계 원칙
1. 리소스 기반 URL 설계
GET /users - 사용자 목록 조회
GET /users/1 - 특정 사용자 조회
POST /users - 사용자 생성
PUT /users/1 - 사용자 전체 수정
PATCH /users/1 - 사용자 부분 수정
DELETE /users/1 - 사용자 삭제
2. HTTP 상태 코드 활용
200 - OK (성공)
201 - Created (생성됨)
204 - No Content (삭제 성공)
400 - Bad Request (잘못된 요청)
401 - Unauthorized (인증 필요)
403 - Forbidden (권한 없음)
404 - Not Found (리소스 없음)
500 - Internal Server Error (서버 오류)
2. 프로젝트 설정
mkdir rest-api
cd rest-api
npm init -y
npm install express cors helmet morgan
npm install -D nodemon
// package.json
{
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js"
}
}
3. 기본 구조
src/
├── index.js
├── routes/
│ ├── index.js
│ └── users.js
├── controllers/
│ └── userController.js
├── services/
│ └── userService.js
├── models/
│ └── User.js
├── middleware/
│ ├── errorHandler.js
│ └── validator.js
└── utils/
└── ApiError.js
반응형
4. API 서버 구현
4.1 메인 서버 파일
// src/index.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const routes = require('./routes');
const errorHandler = require('./middleware/errorHandler');
const app = express();
// 미들웨어
app.use(helmet());
app.use(cors());
app.use(morgan('combined'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 라우트
app.use('/api', routes);
// 404 처리
app.use((req, res) => {
res.status(404).json({
success: false,
error: 'Not Found',
message: `Cannot ${req.method} ${req.path}`
});
});
// 에러 핸들러
app.use(errorHandler);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`API 서버 실행 중: http://localhost:${PORT}`);
});
4.2 라우트 정의
// src/routes/index.js
const express = require('express');
const userRoutes = require('./users');
const router = express.Router();
router.use('/users', userRoutes);
// API 상태 확인
router.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
module.exports = router;
// src/routes/users.js
const express = require('express');
const userController = require('../controllers/userController');
const { validateUser, validateId } = require('../middleware/validator');
const router = express.Router();
router.get('/', userController.getAll);
router.get('/:id', validateId, userController.getById);
router.post('/', validateUser, userController.create);
router.put('/:id', validateId, validateUser, userController.update);
router.patch('/:id', validateId, userController.partialUpdate);
router.delete('/:id', validateId, userController.remove);
module.exports = router;
4.3 컨트롤러
// src/controllers/userController.js
const userService = require('../services/userService');
const ApiError = require('../utils/ApiError');
exports.getAll = async (req, res, next) => {
try {
const { page = 1, limit = 10, sort = 'createdAt', order = 'desc' } = req.query;
const result = await userService.findAll({
page: parseInt(page),
limit: parseInt(limit),
sort,
order
});
res.json({
success: true,
data: result.users,
pagination: {
page: result.page,
limit: result.limit,
total: result.total,
totalPages: result.totalPages
}
});
} catch (error) {
next(error);
}
};
exports.getById = async (req, res, next) => {
try {
const user = await userService.findById(req.params.id);
if (!user) {
throw new ApiError(404, 'User not found');
}
res.json({
success: true,
data: user
});
} catch (error) {
next(error);
}
};
exports.create = async (req, res, next) => {
try {
const user = await userService.create(req.body);
res.status(201).json({
success: true,
data: user
});
} catch (error) {
next(error);
}
};
exports.update = async (req, res, next) => {
try {
const user = await userService.update(req.params.id, req.body);
if (!user) {
throw new ApiError(404, 'User not found');
}
res.json({
success: true,
data: user
});
} catch (error) {
next(error);
}
};
exports.partialUpdate = async (req, res, next) => {
try {
const user = await userService.partialUpdate(req.params.id, req.body);
if (!user) {
throw new ApiError(404, 'User not found');
}
res.json({
success: true,
data: user
});
} catch (error) {
next(error);
}
};
exports.remove = async (req, res, next) => {
try {
const deleted = await userService.remove(req.params.id);
if (!deleted) {
throw new ApiError(404, 'User not found');
}
res.status(204).end();
} catch (error) {
next(error);
}
};
4.4 서비스
// src/services/userService.js
const User = require('../models/User');
class UserService {
constructor() {
this.users = new Map();
this.nextId = 1;
}
async findAll({ page, limit, sort, order }) {
const allUsers = Array.from(this.users.values());
// 정렬
allUsers.sort((a, b) => {
const aVal = a[sort];
const bVal = b[sort];
const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
return order === 'desc' ? -comparison : comparison;
});
// 페이지네이션
const total = allUsers.length;
const totalPages = Math.ceil(total / limit);
const start = (page - 1) * limit;
const users = allUsers.slice(start, start + limit);
return { users, page, limit, total, totalPages };
}
async findById(id) {
return this.users.get(parseInt(id));
}
async create(data) {
const user = new User({
id: this.nextId++,
...data,
createdAt: new Date(),
updatedAt: new Date()
});
this.users.set(user.id, user);
return user;
}
async update(id, data) {
const userId = parseInt(id);
const user = this.users.get(userId);
if (!user) return null;
const updated = new User({
...user,
...data,
id: userId,
updatedAt: new Date()
});
this.users.set(userId, updated);
return updated;
}
async partialUpdate(id, data) {
return this.update(id, data);
}
async remove(id) {
return this.users.delete(parseInt(id));
}
}
module.exports = new UserService();
4.5 모델
// src/models/User.js
class User {
constructor({ id, name, email, phone, role = 'user', createdAt, updatedAt }) {
this.id = id;
this.name = name;
this.email = email;
this.phone = phone;
this.role = role;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
toJSON() {
return {
id: this.id,
name: this.name,
email: this.email,
phone: this.phone,
role: this.role,
createdAt: this.createdAt,
updatedAt: this.updatedAt
};
}
}
module.exports = User;
5. 미들웨어
5.1 에러 핸들러
// src/middleware/errorHandler.js
const ApiError = require('../utils/ApiError');
function errorHandler(err, req, res, next) {
console.error(err);
if (err instanceof ApiError) {
return res.status(err.statusCode).json({
success: false,
error: err.message
});
}
// 검증 에러
if (err.name === 'ValidationError') {
return res.status(400).json({
success: false,
error: 'Validation Error',
details: err.details
});
}
// 기본 에러
res.status(500).json({
success: false,
error: 'Internal Server Error'
});
}
module.exports = errorHandler;
5.2 유효성 검증
// src/middleware/validator.js
const ApiError = require('../utils/ApiError');
exports.validateUser = (req, res, next) => {
const { name, email } = req.body;
const errors = [];
if (!name || name.length < 2) {
errors.push('Name must be at least 2 characters');
}
if (!email || !isValidEmail(email)) {
errors.push('Valid email is required');
}
if (errors.length > 0) {
const error = new Error('Validation Error');
error.name = 'ValidationError';
error.details = errors;
return next(error);
}
next();
};
exports.validateId = (req, res, next) => {
const { id } = req.params;
if (!id || isNaN(parseInt(id))) {
return next(new ApiError(400, 'Invalid ID parameter'));
}
next();
};
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
5.3 API 에러 클래스
// src/utils/ApiError.js
class ApiError extends Error {
constructor(statusCode, message) {
super(message);
this.statusCode = statusCode;
this.name = 'ApiError';
}
static badRequest(message) {
return new ApiError(400, message);
}
static unauthorized(message = 'Unauthorized') {
return new ApiError(401, message);
}
static forbidden(message = 'Forbidden') {
return new ApiError(403, message);
}
static notFound(message = 'Not Found') {
return new ApiError(404, message);
}
static internal(message = 'Internal Server Error') {
return new ApiError(500, message);
}
}
module.exports = ApiError;
6. 고급 기능
6.1 필터링과 검색
// GET /api/users?role=admin&search=john
exports.getAll = async (req, res, next) => {
try {
const { page = 1, limit = 10, role, search } = req.query;
let users = Array.from(userService.users.values());
// 필터링
if (role) {
users = users.filter(u => u.role === role);
}
// 검색
if (search) {
const searchLower = search.toLowerCase();
users = users.filter(u =>
u.name.toLowerCase().includes(searchLower) ||
u.email.toLowerCase().includes(searchLower)
);
}
// 페이지네이션
const total = users.length;
const start = (page - 1) * limit;
const paginatedUsers = users.slice(start, start + parseInt(limit));
res.json({
success: true,
data: paginatedUsers,
pagination: { page: parseInt(page), limit: parseInt(limit), total }
});
} catch (error) {
next(error);
}
};
6.2 관계 리소스
// src/routes/users.js
// 사용자의 게시글 조회
router.get('/:userId/posts', async (req, res, next) => {
try {
const posts = await postService.findByUserId(req.params.userId);
res.json({ success: true, data: posts });
} catch (error) {
next(error);
}
});
// 사용자에게 게시글 추가
router.post('/:userId/posts', async (req, res, next) => {
try {
const post = await postService.create({
...req.body,
userId: parseInt(req.params.userId)
});
res.status(201).json({ success: true, data: post });
} catch (error) {
next(error);
}
});
6.3 응답 포맷팅
// src/middleware/responseFormatter.js
function responseFormatter(req, res, next) {
const originalJson = res.json.bind(res);
res.json = (data) => {
const formatted = {
success: true,
timestamp: new Date().toISOString(),
path: req.path,
...data
};
return originalJson(formatted);
};
next();
}
module.exports = responseFormatter;
7. API 문서화
// 간단한 API 문서 엔드포인트
router.get('/docs', (req, res) => {
res.json({
name: 'User API',
version: '1.0.0',
endpoints: [
{ method: 'GET', path: '/api/users', description: '사용자 목록 조회' },
{ method: 'GET', path: '/api/users/:id', description: '사용자 조회' },
{ method: 'POST', path: '/api/users', description: '사용자 생성' },
{ method: 'PUT', path: '/api/users/:id', description: '사용자 수정' },
{ method: 'DELETE', path: '/api/users/:id', description: '사용자 삭제' }
]
});
});
8. 테스트
// tests/users.test.js
const request = require('supertest');
const app = require('../src/app');
describe('Users API', () => {
let userId;
test('POST /api/users - 사용자 생성', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: '홍길동', email: 'hong@example.com' })
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.name).toBe('홍길동');
userId = response.body.data.id;
});
test('GET /api/users/:id - 사용자 조회', async () => {
const response = await request(app)
.get(`/api/users/${userId}`)
.expect(200);
expect(response.body.data.id).toBe(userId);
});
test('DELETE /api/users/:id - 사용자 삭제', async () => {
await request(app)
.delete(`/api/users/${userId}`)
.expect(204);
});
});
결론
RESTful API는 HTTP 메서드와 상태 코드를 활용하여 리소스를 조작하는 표준적인 방식입니다. Express.js로 라우트, 컨트롤러, 서비스 계층을 분리하여 유지보수하기 쉬운 API를 구축할 수 있습니다. 입력 검증, 에러 처리, 페이지네이션, 필터링을 구현하고, 일관된 응답 형식을 사용하면 클라이언트가 사용하기 편리한 API가 됩니다.
728x90
반응형
'Node.js' 카테고리의 다른 글
| Node.js의 GraphQL API 만들기 (0) | 2026.03.12 |
|---|---|
| Node.js의 서버 사이드 렌더링(SSR) (0) | 2026.03.11 |
| Node.js의 Fastify 프레임워크 (1) | 2026.03.11 |
| Node.js의 NestJS 프레임워크 (0) | 2026.03.10 |
| Node.js의 Hapi.js 프레임워크 (0) | 2026.03.10 |