본문으로 바로가기 본문 바로가기
WIKI 기술 지식 베이스

Comments API 만들기

원문 보기 위키 갱신

이 튜토리얼에서는 D1과 Hono ↗︎를 사용해서 블로그 댓글을 저장하고 조회하는 JSON API를 만들어 볼게요. D1 데이터베이스를 만들고 스키마를 정의한 다음, 데이터베이스에서 읽고 쓰는 GET·POST 엔드포인트를 연결하는 과정을 차근차근 따라 해 볼 거예요.

출처: 문서

본문

사전 준비 (Prerequisites)

  1. Cloudflare 계정 ↗︎에 가입하세요.
  2. Node.js ↗︎를 설치하세요.

Node.js 버전 관리자

Volta ↗︎나 nvm ↗︎ 같은 Node 버전 관리자를 사용하면 권한 문제를 피하고 Node.js 버전을 쉽게 바꿀 수 있어요. 이 가이드에서 뒤에 설명할 Wrangler는 Node 16.17.0 이상 버전을 요구해요.

1. 새 Worker 프로젝트 만들기

  1. 다음을 실행해서 d1-comments-api라는 새 프로젝트를 만드세요.

    npm create cloudflare@latest -- d1-comments-api
    
    yarn create cloudflare d1-comments-api
    
    pnpm create cloudflare@latest d1-comments-api
    

    설정 단계에서 다음 옵션을 선택하세요.

    • What would you like to start with? 는 Hello World example을 선택하세요.
    • Which template would you like to use? 는 Worker only를 선택하세요.
    • Which language do you want to use? 는 TypeScript를 선택하세요.
    • Do you want to use git for version control? 은 Yes를 선택하세요.
    • Do you want to deploy your application? 은 No를 선택하세요 (배포 전에 몇 가지를 변경할 거예요).
  2. 프로젝트 디렉터리로 이동하세요.

    cd d1-comments-api
    

2. Hono 설치하기

Workers에서 API를 만들기 위한 가벼운 웹 프레임워크인 Hono ↗︎를 설치하세요.

npm i hono
yarn add hono
pnpm add hono
bun add hono

3. 데이터베이스 만들기

  1. Wrangler로 새 D1 데이터베이스를 만드세요.

    npx wrangler@latest d1 create d1-comments-api
    
  2. Would you like Wrangler to add it on your behalf?라는 안내가 나오면 Yes를 선택하세요. 그러면 DB 바인딩이 자동으로 Wrangler 설정 파일에 추가돼요.

    Wrangler 설정 파일에 d1_databases 바인딩과 전체 프로젝트 설정이 들어 있는지 확인하세요.

    {
      "$schema": "./node_modules/wrangler/config-schema.json",
      "name": "d1-comments-api",
      "main": "src/index.ts",
      // Set this to today's date
      "compatibility_date": "2026-10-07",
      "d1_databases": [
        {
          "binding": "DB",
          "database_name": "d1-comments-api",
          "database_id": "<YOUR_DATABASE_ID>"
        }
      ]
    }
    
    name = "d1-comments-api"
    main = "src/index.ts"
    # Set this to today's date
    compatibility_date = "2026-10-07"
    
    [[d1_databases]]
    binding = "DB" # available in your Worker on env.DB
    database_name = "d1-comments-api"
    database_id = "<YOUR_DATABASE_ID>"
    

    <YOUR_DATABASE_ID>는 wrangler d1 create 명령이 출력한 ID로 바꿔 주세요.

바인딩(Bindings)을 사용하면 코드 안의 변수 이름으로 D1 데이터베이스, KV 네임스페이스, R2 버킷 같은 리소스에 접근할 수 있어요. 여러분의 D1 데이터베이스는 Worker 안에서 env.DB로 접근할 수 있어요.

4. 스키마를 만들고 데이터베이스 시드(seed)하기

  1. 다음 내용으로 schemas/schema.sql 파일을 만드세요.

    DROP TABLE IF EXISTS comments;
    CREATE TABLE IF NOT EXISTS comments (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      author TEXT NOT NULL,
      body TEXT NOT NULL,
      post_slug TEXT NOT NULL
    );
    CREATE INDEX idx_comments_post_slug ON comments (post_slug);
    
    -- Optionally, uncomment the below query to insert seed data
    -- INSERT INTO comments (author, body, post_slug) VALUES ('Kristian', 'Great post!', 'hello-world');
    
  2. 먼저 로컬 데이터베이스에 스키마를 실행하세요.

    npx wrangler d1 execute d1-comments-api --local --file schemas/schema.sql
    
  3. 로컬에서 테이블이 생성됐는지 확인하세요.

    npx wrangler d1 execute d1-comments-api --local --command "SELECT name FROM sqlite_schema WHERE type = 'table'"
    
    ┌──────────┐
    │ name     │
    ├──────────┤
    │ comments │
    └──────────┘
    
  4. 스키마가 마음에 들면 원격(프로덕션) 데이터베이스에도 적용하세요.

    npx wrangler d1 execute d1-comments-api --remote --file schemas/schema.sql
    

5. Hono 애플리케이션 초기화하기

src/index.ts의 내용을 다음 코드로 바꾸세요. 이 코드는 타입이 지정된 Bindings 인터페이스로 Hono 애플리케이션을 설정해서, env.DB가 D1Database로 올바르게 타입이 지정되게 해 줘요.

import { Hono } from "hono";

const app = new Hono();

app.get("/api/posts/:slug/comments", async (c) => {
	// Do something and return an HTTP response
	// Optionally, do something with c.req.param("slug")
});

app.post("/api/posts/:slug/comments", async (c) => {
	// Do something and return an HTTP response
	// Optionally, do something with c.req.param("slug")
});

export default app;
import { Hono } from "hono";

type Bindings = {
	DB: D1Database;
};

const app = new Hono<{ Bindings: Bindings }>();

app.get("/api/posts/:slug/comments", async (c) => {
	// Do something and return an HTTP response
	// Optionally, do something with c.req.param("slug")
});

app.post("/api/posts/:slug/comments", async (c) => {
	// Do something and return an HTTP response
	// Optionally, do something with c.req.param("slug")
});

export default app;

6. 댓글 조회하기

특정 게시물의 댓글을 가져오는 GET 엔드포인트의 로직을 추가해요. 이 코드는 D1 Workers Binding API를 사용해 매개변수화된 쿼리(parameterized query)를 준비하고 실행해요.

app.get("/api/posts/:slug/comments", async (c) => {
	const { slug } = c.req.param();
	const { results } = await c.env.DB.prepare(
		"SELECT * FROM comments WHERE post_slug = ?",
	)
		.bind(slug)
		.run();
	return c.json(results);
});
app.get("/api/posts/:slug/comments", async (c) => {
	const { slug } = c.req.param();
	const { results } = await c.env.DB.prepare(
		"SELECT * FROM comments WHERE post_slug = ?",
	)
		.bind(slug)
		.run();
	return c.json(results);
});

이 코드는 prepare로 매개변수화된 구문을 만들고, bind로 slug 값을 안전하게 전달하며(SQL 인젝션 방지), run으로 쿼리를 실행해요.

7. 댓글 삽입하기

새 댓글을 만드는 POST 엔드포인트를 추가해요. 이 코드는 행을 삽입하기 전에 요청 본문을 검증해요.

app.post("/api/posts/:slug/comments", async (c) => {
	const { slug } = c.req.param();
	const { author, body } = await c.req.json();

	if (!author) return c.text("Missing author value for new comment", 400);
	if (!body) return c.text("Missing body value for new comment", 400);

	const { success } = await c.env.DB.prepare(
		"INSERT INTO comments (author, body, post_slug) VALUES (?, ?, ?)",
	)
		.bind(author, body, slug)
		.run();

	if (success) {
		c.status(201);
		return c.text("Created");
	} else {
		c.status(500);
		return c.text("Something went wrong");
	}
});
app.post("/api/posts/:slug/comments", async (c) => {
	const { slug } = c.req.param();
	const { author, body } = await c.req.json<{
		author: string;
		body: string;
	}>();

	if (!author) return c.text("Missing author value for new comment", 400);
	if (!body) return c.text("Missing body value for new comment", 400);

	const { success } = await c.env.DB.prepare(
		"INSERT INTO comments (author, body, post_slug) VALUES (?, ?, ?)",
	)
		.bind(author, body, slug)
		.run();

	if (success) {
		c.status(201);
		return c.text("Created");
	} else {
		c.status(500);
		return c.text("Something went wrong");
	}
});

8. (선택) CORS 지원 추가하기

이 API를 다른 오리진(origin)의 프런트엔드 애플리케이션에서 호출할 계획이라면 CORS 미들웨어를 추가하세요. Hono에서 cors 모듈을 가져와 라우트보다 먼저 추가하세요.

import { Hono } from "hono";
import { cors } from "hono/cors";

const app = new Hono();
app.use("/api/*", cors());
import { Hono } from "hono";
import { cors } from "hono/cors";

type Bindings = {
	DB: D1Database;
};

const app = new Hono<{ Bindings: Bindings }>();
app.use("/api/*", cors());

/api/*에 요청을 하면 Hono가 자동으로 API 응답에 CORS 헤더를 생성해 추가해 줘요.

9. 애플리케이션 배포하기

  1. Cloudflare 계정에 로그인하세요 (아직 로그인하지 않았다면).

    npx wrangler whoami
    

    로그인하지 않았다면 Wrangler가 로그인하라고 안내할 거예요.

  2. Worker를 배포하세요.

    npx wrangler deploy
    
  3. 댓글을 삽입한 뒤 조회해서 API를 테스트해 보세요.

    # Replace <YOUR_SUBDOMAIN> with your workers.dev subdomain
    curl -X POST https://d1-comments-api.<YOUR_SUBDOMAIN>.workers.dev/api/posts/hello-world/comments \
      -H "Content-Type: application/json" \
      -d '{"author": "Kristian", "body": "Great post!"}'
    
    Created
    
    curl https://d1-comments-api.<YOUR_SUBDOMAIN>.workers.dev/api/posts/hello-world/comments
    
    [
      {
        "id": 1,
        "author": "Kristian",
        "body": "Great post!",
        "post_slug": "hello-world"
      }
    ]
    

전체 예제 (Full example)

모든 라우트와 CORS 지원이 포함된 완전한 src/index.ts입니다.

import { Hono } from "hono";
import { cors } from "hono/cors";

const app = new Hono();
app.use("/api/*", cors());

app.get("/api/posts/:slug/comments", async (c) => {
	const { slug } = c.req.param();
	const { results } = await c.env.DB.prepare(
		"SELECT * FROM comments WHERE post_slug = ?",
	)
		.bind(slug)
		.run();
	return c.json(results);
});

app.post("/api/posts/:slug/comments", async (c) => {
	const { slug } = c.req.param();
	const { author, body } = await c.req.json();

	if (!author) return c.text("Missing author value for new comment", 400);
	if (!body) return c.text("Missing body value for new comment", 400);

	const { success } = await c.env.DB.prepare(
		"INSERT INTO comments (author, body, post_slug) VALUES (?, ?, ?)",
	)
		.bind(author, body, slug)
		.run();

	if (success) {
		c.status(201);
		return c.text("Created");
	} else {
		c.status(500);
		return c.text("Something went wrong");
	}
});

export default app;
import { Hono } from "hono";
import { cors } from "hono/cors";

type Bindings = {
	DB: D1Database;
};

const app = new Hono<{ Bindings: Bindings }>();
app.use("/api/*", cors());

app.get("/api/posts/:slug/comments", async (c) => {
	const { slug } = c.req.param();
	const { results } = await c.env.DB.prepare(
		"SELECT * FROM comments WHERE post_slug = ?",
	)
		.bind(slug)
		.run();
	return c.json(results);
});

app.post("/api/posts/:slug/comments", async (c) => {
	const { slug } = c.req.param();
	const { author, body } = await c.req.json<{
		author: string;
		body: string;
	}>();

	if (!author) return c.text("Missing author value for new comment", 400);
	if (!body) return c.text("Missing body value for new comment", 400);

	const { success } = await c.env.DB.prepare(
		"INSERT INTO comments (author, body, post_slug) VALUES (?, ?, ?)",
	)
		.bind(author, body, slug)
		.run();

	if (success) {
		c.status(201);
		return c.text("Created");
	} else {
		c.status(500);
		return c.text("Something went wrong");
	}
});

export default app;

다음 단계 (Next steps)

더 알아보기 (Learn more)