1. 내 첫 API는 참사였다
제 첫 번째 API 설계는 정말 끔찍했습니다. /getUserData, /deleteUserById, /updateUserPassword 같은 엔드포인트가 즐비했죠. HTTP 메서드는 전부 POST였고요. "어차피 데이터 보내는 거 아니야?"라고 생각했거든요.
상태 코드는? 전부 200 OK였습니다. 에러가 나도 { "success": false, "error": "뭔가 잘못됨" }을 200으로 내려보냈습니다. 프론트엔드 개발자가 제게 따졌습니다. "이게 에러야 성공이야?" 저는 답했죠. "바디에 success 필드 봐요!"
그 개발자는 화면을 끄고 나갔습니다.
그제서야 받아들였습니다. API는 "나만 쓰는 코드"가 아니라, 남이 쓰는 제품이라는 걸요. 그리고 그 제품에는 약속된 규칙이 있어야 한다는 걸요. 바로 REST였습니다.
2. REST를 레스토랑으로 이해하기
REST API를 처음 배울 때, 이런 비유가 확 와닿았습니다.
레스토랑을 생각해보세요.
- 메뉴판(URI): 손님은 메뉴판을 보고 "돈까스 1번"을 주문합니다. 메뉴 이름은 명사입니다. "돈까스 만들어줘"가 아니라 "돈까스"죠.
- 웨이터(HTTP Method): 손님이 말하는 방식이 다릅니다.
- "메뉴 좀 보여주세요" (GET)
- "돈까스 하나 주세요" (POST)
- "이 테이블 돈까스를 함박스테이크로 바꿔주세요" (PUT)
- "이 음식 치워주세요" (DELETE)
- 주방(Server): 주방은 손님이 누군지, 전에 뭘 먹었는지 기억하지 않습니다(Stateless). 매번 주문서만 봅니다.
- 음식 포장(JSON/XML): 같은 음식이라도 포장 방식이 다를 수 있습니다. 도시락(JSON), 정갈한 접시(XML).
이렇게 정리해본다면, REST API는 웹이라는 거대한 레스토랑의 표준 서비스 방식입니다. 손님(클라이언트)과 주방(서버)이 같은 언어(HTTP)로 대화하는 거죠.
3. REST의 6가지 제약조건 - 핵심 원칙
로이 필딩(Roy Fielding)이 2000년 박사 논문에서 정의한 REST의 철학입니다. 저는 이걸 "인터넷 규칙"처럼 받아들였습니다.
3.1. Client-Server (클라이언트-서버)
관심사의 분리. UI는 클라이언트가, 데이터는 서버가 관리합니다. 이렇게 하면 각자 독립적으로 발전할 수 있습니다.
3.2. Stateless (무상태성) [가장 중요]
서버는 클라이언트의 상태를 저장하지 않습니다. 모든 요청은 자체적으로 완결된 정보를 담아야 합니다. 예를 들어 인증 토큰을 매번 헤더에 넣어야 합니다.
왜 중요할까요? 서버 A가 죽어도, 서버 B가 즉시 대신할 수 있습니다. 서버에 "기억"이 없으니까요. 이게 무한 확장의 비밀입니다.
제 서비스도 처음엔 세션을 썼습니다. 서버를 2대로 늘렸더니 "로그인이 풀렸다 생겼다"하는 버그가 생겼죠. 그때 이해했습니다. Stateless가 왜 필수인지요.
3.3. Cacheable (캐시 가능)
HTTP 헤더(Cache-Control, ETag)를 통해 응답을 캐싱할 수 있어야 합니다.
CDN에 이미지를 캐싱하면 서버는 한 번만 보내고, 1억 명이 같은 이미지를 받아도 서버는 무사합니다.
3.4. Uniform Interface (인터페이스 일관성)
이게 REST의 심장입니다. 4가지 규칙:
- URI로 자원 식별:
/users/1은 "1번 사용자"를 가리킵니다. - 표현으로 조작: JSON, XML 등 표현 형식으로 자원을 변경합니다.
- Self-descriptive Message:
Content-Type: application/json같은 헤더만 봐도 메시지 해석이 가능해야 합니다. - HATEOAS: 하이퍼미디어로 상태 전이를 안내합니다. (뒤에서 자세히 설명)
3.5. Layered System (계층형 시스템)
클라이언트는 서버 앞에 로드 밸런서가 있는지, CDN이 있는지 몰라야 합니다. 투명하게 통신하는 거죠.
3.6. Code on Demand (선택사항)
서버가 클라이언트에게 실행 가능한 코드(JavaScript)를 보낼 수 있습니다. 거의 안 쓰는 옵션이라 넘어가겠습니다.
4. 실제로 배운 URI 설계 법칙
4.1. 명사를 써라
- 좋은 예:
/users,/orders,/products - 나쁜 예:
/getUser,/createOrder,/deleteProduct
처음에 저는 /createUser 같은 URI를 썼습니다. 그런데 프론트엔드 개발자가 물었습니다. "그럼 수정은 /updateUser예요?"
그때 깨달았습니다. 동사를 URI에 넣으면 끝도 없이 늘어난다는 걸요. /createUser, /updateUser, /deleteUser, /softDeleteUser...
결국 이거였다. 자원은 명사로 표현하고, 행위는 HTTP 메서드로 표현하는 거.
4.2. 복수형을 써라
/user/1보다 /users/1이 낫습니다. 컬렉션(Collection)이라는 느낌이 명확하거든요.
4.3. 계층 구조를 활용하라
/users/1/orders: 1번 사용자의 주문 목록/orders/5/items: 5번 주문의 상품 목록
이렇게 하면 자원 간 관계가 URI에 드러납니다.
5. HTTP 메서드 - 의미를 담은 동사
5.1. GET (조회)
- 특징: Safe(서버 상태 변경 안 함), Idempotent(몇 번 호출해도 결과 동일)
- 용도: 데이터 읽기
GET /users/1
5.2. POST (생성)
- 특징: Not Safe, Not Idempotent
- 용도: 새 자원 생성
POST /users
Content-Type: application/json
{
"name": "김철수",
"email": "chulsoo@example.com"
}
성공하면 201 Created와 함께 Location: /users/123 헤더를 돌려줘야 합니다.
5.3. PUT (전체 수정)
- 특징: Idempotent (같은 요청 여러 번 보내도 결과 동일)
- 용도: 자원을 통째로 교체
PUT /users/1
Content-Type: application/json
{
"name": "김철수",
"email": "new@example.com",
"age": 30
}
자원이 없으면 생성하고, 있으면 전부 교체합니다.
5.4. PATCH (부분 수정)
- 용도: 일부 필드만 변경
PATCH /users/1
Content-Type: application/json
{
"email": "new@example.com"
}
5.5. DELETE (삭제)
- 특징: Idempotent
- 용도: 자원 삭제
DELETE /users/1
성공하면 204 No Content (바디 없음)를 주로 씁니다.
6. 상태 코드 - HTTP의 신호등
처음에 저는 모든 응답을 200으로 보냈습니다. 에러 정보는 JSON 안에 담았고요.
{
"success": false,
"error": "유저 없음"
}
이게 왜 문제일까요? HTTP 표준을 무시한 겁니다. 프록시, 로드밸런서, 모니터링 툴은 전부 200만 보고 "정상"이라고 판단합니다. 로그에서 에러를 찾으려면 JSON 바디를 일일이 파싱해야 했죠.
와닿았다. 상태 코드는 "편의 기능"이 아니라 HTTP의 핵심 문법이라는 게.
자주 쓰는 상태 코드
| 코드 | 의미 | 사용 사례 |
|---|---|---|
200 OK | 성공 | GET 요청 성공 |
201 Created | 생성 성공 | POST로 자원 생성 완료 |
204 No Content | 성공했지만 바디 없음 | DELETE 성공 |
400 Bad Request | 클라이언트 실수 | 파라미터 형식 오류 |
401 Unauthorized | 인증 안 됨 | 로그인 필요 |
403 Forbidden | 권한 없음 | 접근 금지 자원 |
404 Not Found | 자원 없음 | 존재하지 않는 URI |
429 Too Many Requests | 요청 과다 | Rate Limit 초과 |
500 Internal Server Error | 서버 에러 | 코드 버그 |
503 Service Unavailable | 서비스 불가 | 점검 중 |
401 vs 403 구분법
- 401: "넌 누구야? (Who are you?)" → 로그인하세요.
- 403: "넌 안 돼. (You can't.)" → 권한이 없어요.
7. 실제 코드 - Express.js로 REST API 만들기
이론만 보면 안 와닿습니다. 코드로 보죠.
const express = require('express');
const app = express();
app.use(express.json());
// 가짜 DB
let users = [
{ id: 1, name: "김철수", email: "chulsoo@example.com" },
{ id: 2, name: "이영희", email: "younghee@example.com" }
];
let nextId = 3;
// GET: 전체 사용자 조회
app.get('/users', (req, res) => {
res.status(200).json(users);
});
// GET: 특정 사용자 조회
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: "User not found" });
}
res.status(200).json(user);
});
// POST: 사용자 생성
app.post('/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: "Name and email required" });
}
const newUser = { id: nextId++, name, email };
users.push(newUser);
res.status(201)
.location(`/users/${newUser.id}`)
.json(newUser);
});
// PUT: 사용자 전체 수정
app.put('/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const { name, email } = req.body;
const index = users.findIndex(u => u.id === id);
if (index === -1) {
// 없으면 생성 (Idempotent)
const newUser = { id, name, email };
users.push(newUser);
return res.status(201).json(newUser);
}
// 있으면 교체
users[index] = { id, name, email };
res.status(200).json(users[index]);
});
// DELETE: 사용자 삭제
app.delete('/users/:id', (req, res) => {
const id = parseInt(req.params.id);
users = users.filter(u => u.id !== id);
res.status(204).send();
});
app.listen(3000, () => {
console.log('REST API server running on port 3000');
});
이 코드에서 주목할 점:
- URI는 명사:
/users - 행위는 메서드:
GET,POST,PUT,DELETE - 상태 코드 정확히 사용:
201은 Location 헤더와 함께,404는 자원 없을 때 - PUT은 Idempotent: 없으면 생성, 있으면 교체
8. 멱등성 (Idempotency) - 결제 중복을 막는 법
결제 API를 만들 때, 저는 큰 실수를 했습니다. 사용자가 "결제" 버튼을 클릭했는데 타임아웃이 났습니다. 결과를 모르니 다시 클릭했죠. 그런데... 두 번 결제됐습니다.
이해했다. POST는 멱등하지 않다는 게. 같은 요청을 보내면 자원이 계속 생깁니다.
해결책은 Idempotency Key였습니다.
동작 방식
- 클라이언트가 요청할 때
Idempotency-Key: uuid헤더를 보냅니다. - 서버는 이 키를 Redis 같은 저장소에 저장합니다.
- 같은 키로 요청이 다시 오면, 로직을 실행하지 않고 저장된 응답을 그대로 반환합니다.
const redis = require('redis');
const client = redis.createClient();
app.post('/payments', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey) {
return res.status(400).json({ error: "Idempotency-Key header required" });
}
// 캐시 확인
const cached = await client.get(`idempotency:${idempotencyKey}`);
if (cached) {
console.log("중복 요청 감지. 캐시된 응답 반환.");
return res.status(200).json(JSON.parse(cached));
}
// 실제 결제 로직
const payment = processPayment(req.body); // 가상 함수
// 캐시 저장 (24시간)
await client.setex(
`idempotency:${idempotencyKey}`,
86400,
JSON.stringify(payment)
);
res.status(201).json(payment);
});
이 패턴 덕분에 결제 중복 사고가 사라졌습니다.
9. HATEOAS: API가 길을 안내한다
HATEOAS(Hypermedia As The Engine Of Application State)는 REST의 최종 레벨입니다.
아이디어는 간단합니다. API 응답에 "다음에 뭘 할 수 있는지" 링크를 포함하는 겁니다.
{
"id": 1,
"status": "pending_payment",
"amount": 50000,
"_links": {
"self": { "href": "/orders/1" },
"pay": { "href": "/orders/1/payment", "method": "POST" },
"cancel": { "href": "/orders/1", "method": "DELETE" }
}
}
주문 상태가 pending_payment이면, 클라이언트는 "결제(pay)"나 "취소(cancel)"를 할 수 있다는 걸 알 수 있습니다.
만약 상태가 shipped(배송중)라면?
{
"id": 1,
"status": "shipped",
"_links": {
"self": { "href": "/orders/1" },
"track": { "href": "/orders/1/tracking", "method": "GET" }
}
}
"결제" 링크가 사라지고 "배송 추적" 링크만 남습니다.
결국 이거였다. 비즈니스 로직이 서버에만 있고, 클라이언트는 링크를 따라가기만 하면 되는 구조. 프론트엔드가 상태 분기 처리를 안 해도 됩니다.
10. 보안 - JWT와 Rate Limiting
10.1. JWT (JSON Web Token)
제 서비스는 JWT로 인증합니다.
로그인 성공 시 서버가 토큰을 줍니다:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
이후 모든 요청에 이 토큰을 헤더에 넣습니다:
GET /users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
서버는 토큰을 검증하고, 유효하면 요청을 처리합니다.
10.2. Rate Limiting
누군가 제 API를 1초에 10000번 호출했습니다. 서버가 죽었습니다.
그때 받아들였습니다. 보안은 "좋으면 좋은 것"이 아니라 필수라는 걸요.
Rate Limiting을 걸었습니다. IP당 1분에 100회로 제한.
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 60 * 1000, // 1분
max: 100, // 최대 100회
message: { error: "Too many requests. Try again later." },
statusCode: 429
});
app.use('/api/', limiter);
위반 시 429 Too Many Requests를 반환합니다.
11. REST vs GraphQL vs gRPC
REST가 만능은 아닙니다. 상황에 따라 다른 선택이 나을 수 있습니다.
| 특징 | REST | GraphQL | gRPC |
|---|---|---|---|
| 철학 | 자원(Resource) | 쿼리(Query) | 함수 호출(RPC) |
| 데이터 형식 | JSON (고정) | JSON (선택적) | Protobuf (바이너리) |
| 엔드포인트 | 여러 개 | 단일 (/graphql) | 여러 개 (함수별) |
| 오버/언더페칭 | 있음 | 없음 (정확히 요청) | 없음 (함수 정의) |
| 성능 | 보통 | 좋음 | 최고 (압축 효율) |
| 캐싱 | 쉬움 (HTTP) | 어려움 (POST 단일) | 어려움 (바이너리) |
| 학습 곡선 | 낮음 | 중간 | 높음 |
| 적합한 용도 | 공개 API, 웹 서비스 | 복잡한 모바일 앱 | 내부 MSA 통신 |
내 경험담
- REST: 제 서비스의 공개 API는 전부 REST입니다. 외부 개발자가 쉽게 쓸 수 있거든요.
- GraphQL: 모바일 앱에서 복잡한 화면(프로필 + 친구 목록 + 최근 활동)을 한 번에 불러올 때 씁니다. 네트워크 요청 횟수가 줄어듭니다.
- gRPC: 내부 마이크로서비스끼리 통신할 때 씁니다. 속도가 압도적으로 빠르거든요.
12. 요약 - REST를 한 줄로
정리해본다면, REST는 "웹의 성공 방식을 API에 적용한 것"입니다.
- 자원은 URI로 표현하고 (명사)
- 행위는 HTTP 메서드로 표현하고 (동사)
- 상태 코드로 결과를 명확히 알리고 (신호등)
- Stateless로 서버를 단순하게 만들고 (확장성)
- 캐싱으로 성능을 높입니다 (효율)
제 첫 API는 엉망이었습니다. 하지만 REST 원칙을 배우고, 실수하고, 고치면서 이해했습니다.
API는 그냥 "데이터 주고받는 통로"가 아니라, 남이 쓰는 제품이라는 걸요. 그리고 그 제품의 품질은 설계에서 결정된다는 걸요.
REST API: I Learned This the Hard Way
1. My First API Was a Disaster
My very first API design was a complete mess. Endpoints like /getUserData, /deleteUserById, /updateUserPassword were everywhere. Every single request? POST. I thought, "We're sending data anyway, right?"
And status codes? Everything was 200 OK. Even when errors occurred, I'd return { "success": false, "error": "something went wrong" } with a 200 status.
A frontend developer confronted me: "Is this an error or success?"
I replied confidently: "Check the success field in the body!"
They closed their laptop and walked away.
That's when it hit me. An API isn't "code only I use" — it's a product others rely on. And that product needs rules, standards, conventions. That's REST.
2. Understanding REST Through a Restaurant Analogy
When learning REST, this metaphor made everything click:
Think of a restaurant.
- Menu (URI): Customers look at the menu and order "Pork Cutlet #1". Menu items are nouns. Not "make me a pork cutlet" — just "pork cutlet".
- Waiter (HTTP Method): Customers communicate differently:
- "Can I see the menu?" (GET)
- "I'll have one pork cutlet" (POST)
- "Replace this dish with a burger" (PUT)
- "Take this away" (DELETE)
- Kitchen (Server): The kitchen doesn't remember who you are or what you ate before (Stateless). They only read the order slip each time.
- Packaging (JSON/XML): Same food, different packaging — lunchbox (JSON), fancy plate (XML).
REST API is the standard service model of the web's giant restaurant. Customers (clients) and kitchens (servers) speak the same language (HTTP).
3. The 6 Constraints of REST: Core Principles
Roy Fielding defined REST in his 2000 PhD dissertation. I think of these as "internet bylaws".
3.1. Client-Server
Separation of concerns. UI handled by client, data by server. Each evolves independently.
3.2. Stateless [Most Critical]
The server stores no client state between requests. Every request must be self-contained with all necessary information (like authentication tokens in headers).
Why does this matter? If Server A crashes, Server B can immediately take over. No "memory" to transfer. This is the secret to infinite scaling.
My service used sessions initially. When I scaled to 2 servers, users complained: "I keep getting logged out randomly." That's when I understood why Stateless is non-negotiable.
3.3. Cacheable
Responses should indicate if they can be cached via HTTP headers (Cache-Control, ETag).
Cache an image on a CDN once, and the server remains untouched even if 100 million users download it.
3.4. Uniform Interface
This is REST's heart. Four rules:
- Resources identified by URIs:
/users/1points to "user #1". - Manipulate via representations: Use JSON, XML, etc. to modify resources.
- Self-descriptive messages: Headers like
Content-Type: application/jsonmake messages interpretable without external docs. - HATEOAS: Hypermedia guides state transitions. (Detailed later)
3.5. Layered System
The client shouldn't know if there's a load balancer or CDN in front of the server. Communication should be transparent.
3.6. Code on Demand (Optional)
Servers can send executable code (JavaScript) to clients. Rarely used, so we'll skip this.
4. URI Design Rules I Learned the Hard Way
4.1. Use Nouns
- Good:
/users,/orders,/products - Bad:
/getUser,/createOrder,/deleteProduct
Initially, I used URIs like /createUser. Then a frontend dev asked: "So updates are /updateUser?"
That's when I realized: if you put verbs in URIs, they multiply endlessly. /createUser, /updateUser, /deleteUser, /softDeleteUser...
The insight: Resources are nouns. Actions are HTTP methods.
4.2. Use Plural Forms
/users/1 is better than /user/1. It clearly signals a collection.
4.3. Leverage Hierarchy
/users/1/orders: Orders for user #1/orders/5/items: Items in order #5
This structure reveals relationships between resources directly in the URI.
5. HTTP Methods: Verbs with Meaning
5.1. GET (Read)
- Properties: Safe (no server state change), Idempotent (same result on multiple calls)
- Use case: Reading data
GET /users/1
5.2. POST (Create)
- Properties: Not Safe, Not Idempotent
- Use case: Creating new resources
POST /users
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com"
}
On success, return 201 Created with Location: /users/123 header.
5.3. PUT (Full Update)
- Properties: Idempotent (same request repeated yields same result)
- Use case: Completely replace a resource
PUT /users/1
Content-Type: application/json
{
"name": "John Doe",
"email": "newemail@example.com",
"age": 30
}
If the resource doesn't exist, create it. If it exists, replace it entirely.
5.4. PATCH (Partial Update)
- Use case: Update only some fields
PATCH /users/1
Content-Type: application/json
{
"email": "newemail@example.com"
}
5.5. DELETE (Remove)
- Properties: Idempotent
- Use case: Delete resource
DELETE /users/1
Typically returns 204 No Content (no body).
6. Status Codes: HTTP's Traffic Lights
Initially, I returned everything as 200. Error details were buried in JSON:
{
"success": false,
"error": "User not found"
}
Why is this wrong? It ignores HTTP standards. Proxies, load balancers, monitoring tools see 200 and assume "success". Finding errors required parsing every JSON body.
That's when it clicked: status codes aren't a "nice-to-have" — they're core HTTP grammar.
Common Status Codes
| Code | Meaning | Use Case |
|---|---|---|
200 OK | Success | GET request succeeded |
201 Created | Created | POST created resource |
204 No Content | Success, no body | DELETE succeeded |
400 Bad Request | Client error | Malformed parameters |
401 Unauthorized | Not authenticated | Login required |
403 Forbidden | Not authorized | Insufficient permissions |
404 Not Found | Resource missing | Non-existent URI |
429 Too Many Requests | Rate limited | Exceeded quota |
500 Internal Server Error | Server error | Code bug |
503 Service Unavailable | Service down | Maintenance mode |
401 vs 403: A Simple Rule
- 401: "Who are you?" → Need to log in.
- 403: "You can't do this." → Insufficient permissions.
7. Real Code: Building REST API with Express.js
Theory only goes so far. Let's see code.
const express = require('express');
const app = express();
app.use(express.json());
// Mock database
let users = [
{ id: 1, name: "John Doe", email: "john@example.com" },
{ id: 2, name: "Jane Smith", email: "jane@example.com" }
];
let nextId = 3;
// GET: List all users
app.get('/users', (req, res) => {
res.status(200).json(users);
});
// GET: Get single user
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: "User not found" });
}
res.status(200).json(user);
});
// POST: Create user
app.post('/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: "Name and email required" });
}
const newUser = { id: nextId++, name, email };
users.push(newUser);
res.status(201)
.location(`/users/${newUser.id}`)
.json(newUser);
});
// PUT: Full update
app.put('/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const { name, email } = req.body;
const index = users.findIndex(u => u.id === id);
if (index === -1) {
// Create if doesn't exist (Idempotent)
const newUser = { id, name, email };
users.push(newUser);
return res.status(201).json(newUser);
}
// Replace if exists
users[index] = { id, name, email };
res.status(200).json(users[index]);
});
// DELETE: Remove user
app.delete('/users/:id', (req, res) => {
const id = parseInt(req.params.id);
users = users.filter(u => u.id !== id);
res.status(204).send();
});
app.listen(3000, () => {
console.log('REST API server running on port 3000');
});
Key takeaways:
- URIs are nouns:
/users - Actions are methods:
GET,POST,PUT,DELETE - Correct status codes:
201with Location header,404when not found - PUT is idempotent: Creates if absent, replaces if present
8. Idempotency: Preventing Double Charges
When building a payment API, I made a critical mistake. A user clicked "Pay". The request timed out. They clicked again. Result? Charged twice.
That's when I learned: POST is not idempotent. Send the same request twice, you create the resource twice.
The solution: Idempotency Keys.
How It Works
- Client sends
Idempotency-Key: uuidheader with the request. - Server stores this key in Redis or similar.
- If the same key arrives again, skip logic and return the cached response.
const redis = require('redis');
const client = redis.createClient();
app.post('/payments', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey) {
return res.status(400).json({ error: "Idempotency-Key header required" });
}
// Check cache
const cached = await client.get(`idempotency:${idempotencyKey}`);
if (cached) {
console.log("Duplicate request detected. Returning cached response.");
return res.status(200).json(JSON.parse(cached));
}
// Actual payment logic
const payment = processPayment(req.body); // Mock function
// Store in cache (24h TTL)
await client.setex(
`idempotency:${idempotencyKey}`,
86400,
JSON.stringify(payment)
);
res.status(201).json(payment);
});
This pattern eliminated double-charge incidents completely.
9. HATEOAS: API That Guides You
HATEOAS (Hypermedia As The Engine Of Application State) is REST's final level.
The idea is simple: Include "what you can do next" links in API responses.
{
"id": 1,
"status": "pending_payment",
"amount": 50000,
"_links": {
"self": { "href": "/orders/1" },
"pay": { "href": "/orders/1/payment", "method": "POST" },
"cancel": { "href": "/orders/1", "method": "DELETE" }
}
}
If the order status is pending_payment, clients know they can "pay" or "cancel".
What if status is shipped?
{
"id": 1,
"status": "shipped",
"_links": {
"self": { "href": "/orders/1" },
"track": { "href": "/orders/1/tracking", "method": "GET" }
}
}
The "pay" link disappears, only "track" remains.
The insight: Business logic stays on the server. Clients just follow links. Frontend doesn't need complex state branching.
10. Security: JWT and Rate Limiting
10.1. JWT (JSON Web Token)
My service uses JWT for authentication.
On successful login, server returns a token:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
All subsequent requests include this token in headers:
GET /users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Server validates the token and processes the request if valid.
10.2. Rate Limiting
Someone called my API 10,000 times per second. The server died.
That's when I learned: security isn't "nice to have" — it's mandatory.
I implemented rate limiting: 100 requests per minute per IP.
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // max 100 requests
message: { error: "Too many requests. Try again later." },
statusCode: 429
});
app.use('/api/', limiter);
Violations return 429 Too Many Requests.
11. REST vs GraphQL vs gRPC
REST isn't a silver bullet. Different tools for different contexts.
| Feature | REST | GraphQL | gRPC |
|---|---|---|---|
| Philosophy | Resource-oriented | Query-oriented | Function call (RPC) |
| Data Format | JSON (fixed) | JSON (selective) | Protobuf (binary) |
| Endpoints | Multiple | Single (/graphql) | Multiple (per function) |
| Over/Under-fetching | Yes | No (precise queries) | No (function-defined) |
| Performance | Medium | Good | Excellent (compression) |
| Caching | Easy (HTTP) | Hard (POST single) | Hard (binary) |
| Learning Curve | Low | Medium | High |
| Best For | Public APIs, web services | Complex mobile apps | Internal microservices |
My Experience
- REST: All public APIs in my service use REST. Easy for external developers.
- GraphQL: Mobile app uses it for complex screens (profile + friends + recent activity in one query). Reduces network requests dramatically.
- gRPC: Internal microservices use it. Speed is unbeatable.