Node.js 교재 · 15편 · REST API

JSON API 만들기 — REST 설계와 CRUD

13·14편 위에 표준 REST 규약을 얹어 진짜 API 다운 API.

REST API 엔드포인트와 CRUD 컨셉 일러스트

Express 로 라우트는 만들 줄 안다. 그런데 어떤 경로명을 쓰고, 어떤 status 를 돌려주고, 어떤 응답 모양이 표준인가 — 회사·팀마다 달라서 일관성이 안 나오면 프론트엔드 개발자가 매번 묻는다. 답이 REST.

REST 는 거창한 이론이지만 실전에선 5~6가지 규약만 지키면 90% 의 가치가 나온다. 이번 편이 그 핵심.

1. 리소스 명사 + HTTP 메서드 동사

REST 의 첫 번째 원칙. URL 은 명사, 동사는 HTTP 메서드.

의도RESTful안티패턴
글 목록GET /postsGET /getAllPosts
글 한 개GET /posts/:idGET /getPostById
새 글POST /postsPOST /createPost
글 수정PUT /posts/:idPOST /updatePost
글 삭제DELETE /posts/:idPOST /deletePost
댓글 목록GET /posts/:id/commentsGET /getCommentsOfPost

중첩 리소스도 같은 규칙 — /posts/:id/comments. 복수형을 일관되게 (/post X, /posts O). 동작은 URL 에 적지 말고 메서드로.

2. status 코드 — 5가지면 충분

HTTP status 가 70여 개 있지만 일상에선 6~7 개만 쓴다.

코드의미언제
200 OK성공GET·PUT·PATCH 성공
201 Created생성됨POST 성공 (새 리소스 생성)
204 No Content성공, 본문 없음DELETE 성공
400 Bad Request입력 잘못검증 실패
401 Unauthorized로그인 필요인증 토큰 없음
403 Forbidden권한 없음로그인은 됐지만 접근 불가
404 Not Found없음리소스 없음
500 Internal서버 에러예상 못한 예외

가장 자주 헷갈리는 게 401 vs 403. 401 = "당신 누구?" (인증 안 됨), 403 = "당신은 알지만 권한 없음" (인증은 됐는데 권한 부족).

3. 표준 응답 모양 — 일관성

응답 JSON 의 모양이 매번 다르면 프론트가 죽는다. 한 프로젝트 안에선 무조건 통일.

// 단일 리소스 { "id": "abc", "title": "안녕", "body": "..." } // 목록 (페이지네이션 포함) { "data": [...], "page": 1, "perPage": 20, "total": 137, "hasNext": true } // 에러 { "error": { "code": "VALIDATION_FAILED", "message": "제목은 2자 이상", "details": { "title": ["too short"] } } }

특히 에러 모양은 강력히 규약화 — 클라이언트가 err.error.code 만 검사하면 분기 가능. 한글 메시지는 message, 프로그램 분기용 키는 code (영문 enum).

4. 페이지네이션·필터·정렬 — 쿼리 표준

목록 API 의 3대 친구. 쿼리 파라미터로.

// 클라이언트 호출 GET /posts?page=2&perPage=20&author=jspark&sort=-createdAt // 서버 router.get('/posts', async (req, res) => { const page = Math.max(1, Number(req.query.page) || 1); const perPage = Math.min(50, Number(req.query.perPage) || 20); const skip = (page - 1) * perPage; const where = {}; if (req.query.author) where.author = req.query.author; // sort: 앞에 - 면 desc, 아니면 asc const sortKey = (req.query.sort || '-createdAt').replace(/^-/, ''); const sortDir = String(req.query.sort).startsWith('-') ? -1 : 1; const [items, total] = await Promise.all([ db.posts.find(where).sort({ [sortKey]: sortDir }).skip(skip).limit(perPage), db.posts.count(where), ]); res.json({ data: items, page, perPage, total, hasNext: skip + items.length < total }); });

관례 — page·perPage (offset 페이지네이션) 또는 cursor (커서 기반, 큰 데이터셋). 정렬 prefix - 는 GitHub/Stripe 가 쓰는 표준 패턴.

perPage 상한 필수?perPage=1000000 으로 DB 와 메모리를 무릎 꿇리는 공격이 흔하다. 반드시 Math.min(50, ...) 같은 상한. 안 그러면 DoS.

5. 완전한 CRUD 예 — posts 리소스

// routes/posts.js import { Router } from 'express'; import { z } from 'zod'; const router = Router(); const PostSchema = z.object({ title: z.string().min(2).max(100), body: z.string().min(10), }); // GET /posts — 목록 router.get('/', async (req, res) => { const posts = await db.posts.findMany(); res.json({ data: posts }); }); // GET /posts/:id — 단일 router.get('/:id', async (req, res) => { const post = await db.posts.findUnique({ where: { id: req.params.id } }); if (!post) return res.status(404).json({ error: { code: 'NOT_FOUND' } }); res.json(post); }); // POST /posts — 생성 router.post('/', requireAuth, async (req, res) => { const parsed = PostSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: { code: 'VALIDATION_FAILED', details: parsed.error.flatten() }, }); } const post = await db.posts.create({ data: { ...parsed.data, authorId: req.user.id } }); res.status(201).json(post); }); // PUT /posts/:id — 수정 router.put('/:id', requireAuth, requireOwner, async (req, res) => { const parsed = PostSchema.safeParse(req.body); if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() }); const post = await db.posts.update({ where: { id: req.params.id }, data: parsed.data }); res.json(post); }); // DELETE /posts/:id — 삭제 router.delete('/:id', requireAuth, requireOwner, async (req, res) => { await db.posts.delete({ where: { id: req.params.id } }); res.status(204).end(); }); export default router;

이 한 파일이 표준 REST 리소스의 모범. 검증(Zod)·인증(requireAuth)·소유권 확인(requireOwner)·적절한 status 코드·일관된 응답 모양 다섯이 모두 들어있다. 14편의 미들웨어 패턴 응용.

요약 — 15편 좌표

여기까지 정리. REST 의 5규칙 — URL 은 명사·메서드는 동사, 복수형 일관, 중첩 리소스 같은 패턴, 표준 status 6~7개, 응답 모양 통일(data·error). 페이지네이션·필터·정렬은 쿼리 파라미터 + 상한 강제. CRUD 한 리소스에 검증·인증·소유권 확인이 미들웨어로 끼인다. 다음 편에서 진짜 에러 처리 — try/catch 와 Express 에러 미들웨어.

다음 편 예고 — 에러 처리

try/catch, Express 에러 미들웨어, 사용자 친화적 메시지. 16편.

© 2026 주나이테크(주) @JUNAITECH