Stage 3: Backend Essentials

Node.js · Lesson 05

REST API

Designing GET, POST, PUT and DELETE endpoints that return JSON.

Node.js3 min readFree lesson

REST is a set of conventions for exposing data over HTTP. Once you follow them, any client — a browser, a mobile app, a script — can use your API without reading your code.

Learning objectives

  • Model data as resources and URLs.
  • Choose the right HTTP method and status code.
  • Build a CRUD API with Express.
  • Validate input and return useful errors.

1. Resources and URLs

A resource is a noun, plural, and stable:

/api/memos          the collection
/api/memos/42       one memo

Verbs belong in the HTTP method, not the URL. GET /api/getMemos is not REST; GET /api/memos is.

2. Methods and status codes

Method Purpose Success
GET read 200 OK
POST create 201 Created
PUT / PATCH replace / update 200 OK
DELETE remove 200 OK or 204 No Content

Errors: 400 invalid input, 401 not signed in, 403 not allowed, 404 not found, 409 conflict, 500 server bug.

3. A complete CRUD API

const express = require("express");
const app = express();

app.use(express.json());

let memos = [
  { id: 1, title: "First memo", content: "Hello", createdAt: new Date().toISOString() },
];

// list, with optional filter and sort
app.get("/api/memos", (req, res) => {
  let result = [...memos];

  if (req.query.title) {
    const q = String(req.query.title).toLowerCase();
    result = result.filter((m) => m.title.toLowerCase().includes(q));
  }

  result.sort((a, b) =>
    req.query.sort === "oldest" ? a.id - b.id : b.id - a.id,
  );

  res.json({ success: true, count: result.length, data: result });
});

// read one
app.get("/api/memos/:id", (req, res) => {
  const memo = memos.find((m) => m.id === Number(req.params.id));
  if (!memo) {
    return res.status(404).json({ success: false, error: "Memo not found" });
  }
  res.json({ success: true, data: memo });
});

// create
app.post("/api/memos", (req, res) => {
  const { title, content } = req.body;

  if (!title || !title.trim()) {
    return res.status(400).json({ success: false, error: "title is required" });
  }

  const memo = {
    id: Date.now(),
    title: title.trim(),
    content: (content ?? "").trim(),
    createdAt: new Date().toISOString(),
  };

  memos.push(memo);
  res.status(201).json({ success: true, data: memo });
});

// update
app.put("/api/memos/:id", (req, res) => {
  const memo = memos.find((m) => m.id === Number(req.params.id));
  if (!memo) {
    return res.status(404).json({ success: false, error: "Memo not found" });
  }

  const { title, content } = req.body;
  if (!title || !title.trim()) {
    return res.status(400).json({ success: false, error: "title is required" });
  }

  memo.title = title.trim();
  if (content !== undefined) memo.content = content.trim();

  res.json({ success: true, data: memo });
});

// delete
app.delete("/api/memos/:id", (req, res) => {
  const index = memos.findIndex((m) => m.id === Number(req.params.id));
  if (index === -1) {
    return res.status(404).json({ success: false, error: "Memo not found" });
  }

  const [deleted] = memos.splice(index, 1);
  res.json({ success: true, data: deleted });
});

app.listen(3000);

4. Rules that keep APIs predictable

  • Always return the same envelope: { success, data } or { success, error }.
  • Validate at the boundary. Never trust req.body.
  • return after sending an error response, otherwise the handler continues and Express throws "headers already sent".
  • Keep IDs stable. Date.now() is fine for a demo; a database handles this properly in the next subject.

5. Testing without a frontend

curl http://localhost:3000/api/memos
curl -X POST http://localhost:3000/api/memos \
  -H "Content-Type: application/json" \
  -d "{\"title\":\"Test\",\"content\":\"Body\"}"

Common mistakes

  • Using GET for anything that changes data.
  • Returning 200 for a created resource or for an error.
  • Forgetting express.json(), then reading an empty req.body.
  • Comparing req.params.id (a string) with numeric IDs using ===.

Practice

Implement the memo API in the assignment. Test every status code with curl before writing any frontend.

Example files

Complete, runnable files used in this lesson. Copy them into your project and experiment.

api.jsJavaScript
// ===================================================
// 강의 05 예제 — 할 일 목록 REST API
// 실행: node api.js
// 테스트: http://localhost:3000/api/todos
//
// 사전 준비:
//   npm init -y
//   npm install express
// ===================================================

const express = require('express');
const app = express();
const PORT = 3000;

// ① JSON 요청 바디 파싱 미들웨어 (필수)
app.use(express.json());

// ② 인메모리 데이터 저장소 (서버 재시작 시 초기화)
let todos = [
  { id: 1, title: 'Node.js 강의 듣기', done: false },
  { id: 2, title: 'Express 예제 실습', done: true },
  { id: 3, title: 'REST API 만들기', done: false },
];

// ID 자동 증가를 위한 카운터
let nextId = 4;

// ③ 헬퍼 함수 — ID로 할 일 찾기
const findTodo = (id) => todos.find((t) => t.id === id);

// ============================================================
// 라우트 정의
// ============================================================

// GET /api/todos — 전체 할 일 목록 조회
app.get('/api/todos', (req, res) => {
  // 쿼리스트링으로 필터링 가능: /api/todos?done=true
  const { done } = req.query;

  let result = todos;
  if (done !== undefined) {
    // 'true'/'false' 문자열을 boolean으로 변환
    const isDone = done === 'true';
    result = todos.filter((t) => t.done === isDone);
  }

  res.json({
    success: true,
    count: result.length,
    data: result,
  });
});

// GET /api/todos/:id — 특정 할 일 조회
app.get('/api/todos/:id', (req, res) => {
  const id = parseInt(req.params.id); // URL 파라미터는 문자열이므로 숫자 변환
  const todo = findTodo(id);

  if (!todo) {
    // 없으면 404 응답
    return res.status(404).json({
      success: false,
      error: `ID ${id}인 할 일을 찾을 수 없습니다.`,
    });
  }

  res.json({ success: true, data: todo });
});

// POST /api/todos — 새 할 일 생성
app.post('/api/todos', (req, res) => {
  const { title, done } = req.body;

  // 유효성 검사 — title 필수
  if (!title || title.trim() === '') {
    return res.status(400).json({
      success: false,
      error: 'title 필드는 필수입니다.',
    });
  }

  // 새 할 일 객체 생성
  const newTodo = {
    id: nextId++,          // ID 자동 부여 후 증가
    title: title.trim(),   // 앞뒤 공백 제거
    done: done || false,   // done 기본값 false
  };

  todos.push(newTodo); // 배열에 추가

  // 201 Created: 생성 성공 시 사용하는 상태 코드
  res.status(201).json({
    success: true,
    message: '할 일이 생성되었습니다.',
    data: newTodo,
  });
});

// PUT /api/todos/:id — 할 일 전체 수정
app.put('/api/todos/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const todo = findTodo(id);

  if (!todo) {
    return res.status(404).json({
      success: false,
      error: `ID ${id}인 할 일을 찾을 수 없습니다.`,
    });
  }

  const { title, done } = req.body;

  // 유효성 검사
  if (!title || title.trim() === '') {
    return res.status(400).json({
      success: false,
      error: 'title 필드는 필수입니다.',
    });
  }

  // 기존 객체 수정 (전체 교체)
  todo.title = title.trim();
  todo.done = done !== undefined ? done : todo.done;

  res.json({
    success: true,
    message: '할 일이 수정되었습니다.',
    data: todo,
  });
});

// DELETE /api/todos/:id — 할 일 삭제
app.delete('/api/todos/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const index = todos.findIndex((t) => t.id === id); // 인덱스 찾기

  if (index === -1) {
    return res.status(404).json({
      success: false,
      error: `ID ${id}인 할 일을 찾을 수 없습니다.`,
    });
  }

  // splice로 배열에서 제거 (인덱스, 삭제 개수)
  const deleted = todos.splice(index, 1)[0];

  res.json({
    success: true,
    message: '할 일이 삭제되었습니다.',
    data: deleted,
  });
});

// 서버 시작
app.listen(PORT, () => {
  console.log(`할 일 REST API 서버: http://localhost:${PORT}`);
  console.log('\n=== 사용 가능한 엔드포인트 ===');
  console.log('GET    /api/todos       - 전체 조회');
  console.log('GET    /api/todos/:id   - 단건 조회');
  console.log('POST   /api/todos       - 생성');
  console.log('PUT    /api/todos/:id   - 수정');
  console.log('DELETE /api/todos/:id   - 삭제');
});

Assignment

Assignment 05 — Memo REST API

Build a complete CRUD API for memos, backed by an in-memory array.

Endpoints

Method Path Result
GET /api/memos?title=&sort=oldest list, filtered and sorted
GET /api/memos/:id one memo or 404
POST /api/memos create, 201
PUT /api/memos/:id update, 200
DELETE /api/memos/:id delete, 200

Requirements

  1. express.json() must be registered before every route.
  2. Every response uses the shape { success, data } or { success, error }.
  3. POST and PUT reject a missing or blank title with 400.
  4. GET, PUT and DELETE return 404 for an unknown id.
  5. GET filters by title (case-insensitive substring) and sorts newest-first by default, oldest-first with sort=oldest.
  6. IDs are numbers; convert req.params.id with Number(...) before comparing.
  7. POST sets createdAt to an ISO timestamp.

Hints

const id = Number(req.params.id);
const memo = memos.find((m) => m.id === id);

if (!memo) {
  return res.status(404).json({ success: false, error: "Memo not found" });
}

Test with curl

curl http://localhost:3000/api/memos
curl -X POST http://localhost:3000/api/memos \
  -H "Content-Type: application/json" \
  -d "{\"title\":\"Groceries\",\"content\":\"Milk\"}"
curl -X DELETE http://localhost:3000/api/memos/1
curl -i http://localhost:3000/api/memos/999

Done when

  • Every endpoint returns the documented status code.
  • curl -i shows 201 on create and 404 on a missing id.
  • Filtering and sorting work together.
Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

api.jsJavaScript
// ===================================================
// 과제 05 정답 — 메모 REST API
// 실행: node api.js
// 테스트: http://localhost:3000/api/memos
// ===================================================

const express = require('express');
const app = express();
const PORT = 3000;

// JSON 요청 바디 파싱 미들웨어
app.use(express.json());

// ① 인메모리 메모 저장소 (초기 더미 데이터 포함)
let memos = [
  {
    id: Date.now() - 2000,                          // ID: 현재 시간 기반
    title: '첫 번째 메모',
    content: 'Node.js REST API 첫 번째 메모입니다.',
    createdAt: new Date(Date.now() - 2000).toISOString(), // 생성 시각 ISO 형식
  },
  {
    id: Date.now() - 1000,
    title: '두 번째 메모',
    content: 'Express로 REST API를 만드는 중입니다.',
    createdAt: new Date(Date.now() - 1000).toISOString(),
  },
];

// ② 헬퍼: 숫자 ID로 메모 찾기
const findMemo = (id) => memos.find((m) => m.id === id);

// ============================================================
// GET /api/memos — 전체 메모 조회 (검색 + 정렬 지원)
// ============================================================
app.get('/api/memos', (req, res) => {
  const { title, sort } = req.query;

  let result = [...memos]; // 원본 배열을 건드리지 않도록 복사

  // 제목 검색 필터: title 쿼리스트링이 있으면 포함 검색
  if (title) {
    result = result.filter((m) =>
      m.title.toLowerCase().includes(title.toLowerCase())
    );
  }

  // 정렬: 기본값은 newest (최신순 = id 내림차순)
  if (sort === 'oldest') {
    result.sort((a, b) => a.id - b.id); // 오래된 순 (오름차순)
  } else {
    result.sort((a, b) => b.id - a.id); // 최신 순 (내림차순)
  }

  res.json({
    success: true,
    count: result.length,
    data: result,
  });
});

// ============================================================
// GET /api/memos/:id — 특정 메모 조회
// ============================================================
app.get('/api/memos/:id', (req, res) => {
  // URL 파라미터는 문자열 → 숫자로 변환 (Date.now()는 숫자)
  const id = Number(req.params.id);
  const memo = findMemo(id);

  if (!memo) {
    return res.status(404).json({
      success: false,
      error: `ID ${id}인 메모를 찾을 수 없습니다.`,
    });
  }

  res.json({ success: true, data: memo });
});

// ============================================================
// POST /api/memos — 새 메모 생성
// ============================================================
app.post('/api/memos', (req, res) => {
  const { title, content } = req.body;

  // title은 필수값 — 없으면 400 오류 반환
  if (!title || title.trim() === '') {
    return res.status(400).json({
      success: false,
      error: 'title 필드는 필수입니다.',
    });
  }

  // 새 메모 객체 생성
  const newMemo = {
    id: Date.now(),                    // 유닉스 타임스탬프를 ID로 사용
    title: title.trim(),              // 앞뒤 공백 제거
    content: (content || '').trim(), // content는 선택 사항
    createdAt: new Date().toISOString(), // 생성 시각 ISO 형식으로 저장
  };

  memos.push(newMemo); // 배열에 추가

  // 201 Created: 자원 생성 성공 시 사용
  res.status(201).json({
    success: true,
    message: '메모가 생성되었습니다.',
    data: newMemo,
  });
});

// ============================================================
// PUT /api/memos/:id — 메모 수정
// ============================================================
app.put('/api/memos/:id', (req, res) => {
  const id = Number(req.params.id);
  const memo = findMemo(id);

  // 해당 ID의 메모가 없으면 404 반환
  if (!memo) {
    return res.status(404).json({
      success: false,
      error: `ID ${id}인 메모를 찾을 수 없습니다.`,
    });
  }

  const { title, content } = req.body;

  // 수정 요청에도 title 필수
  if (!title || title.trim() === '') {
    return res.status(400).json({
      success: false,
      error: 'title 필드는 필수입니다.',
    });
  }

  // 기존 메모 직접 수정 (createdAt은 변경하지 않음)
  memo.title = title.trim();
  memo.content = content !== undefined ? content.trim() : memo.content;

  res.json({
    success: true,
    message: '메모가 수정되었습니다.',
    data: memo,
  });
});

// ============================================================
// DELETE /api/memos/:id — 메모 삭제
// ============================================================
app.delete('/api/memos/:id', (req, res) => {
  const id = Number(req.params.id);
  const index = memos.findIndex((m) => m.id === id); // 인덱스 찾기

  if (index === -1) {
    return res.status(404).json({
      success: false,
      error: `ID ${id}인 메모를 찾을 수 없습니다.`,
    });
  }

  // splice(시작인덱스, 삭제개수) — 삭제된 요소를 반환
  const [deleted] = memos.splice(index, 1);

  res.json({
    success: true,
    message: '메모가 삭제되었습니다.',
    data: deleted,
  });
});

// 서버 시작
app.listen(PORT, () => {
  console.log(`메모 REST API 서버: http://localhost:${PORT}`);
  console.log('\n=== 엔드포인트 목록 ===');
  console.log('GET    /api/memos            - 전체 조회 (title=검색어, sort=oldest)');
  console.log('GET    /api/memos/:id        - 단건 조회');
  console.log('POST   /api/memos            - 생성 (body: title, content)');
  console.log('PUT    /api/memos/:id        - 수정 (body: title, content)');
  console.log('DELETE /api/memos/:id        - 삭제');
});

Learn this lesson with a mentor

Work through the Build Your Homepage series on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now