Comments API 만들기
이 튜토리얼에서는 D1과 Hono ↗︎를 사용해서 블로그 댓글을 저장하고 조회하는 JSON API를 만들어 볼게요. D1 데이터베이스를 만들고 스키마를 정의한 다음, 데이터베이스에서 읽고 쓰는 GET·POST 엔드포인트를 연결하는 과정을 차근차근 따라 해 볼 거예요.
출처: 문서
본문
사전 준비 (Prerequisites)
- Cloudflare 계정 ↗︎에 가입하세요.
Node.js↗︎를 설치하세요.
Node.js 버전 관리자
Volta ↗︎나 nvm ↗︎ 같은 Node 버전 관리자를 사용하면 권한 문제를 피하고 Node.js 버전을 쉽게 바꿀 수 있어요. 이 가이드에서 뒤에 설명할 Wrangler는 Node 16.17.0 이상 버전을 요구해요.
1. 새 Worker 프로젝트 만들기
-
다음을 실행해서
d1-comments-api라는 새 프로젝트를 만드세요.npm create cloudflare@latest -- d1-comments-apiyarn create cloudflare d1-comments-apipnpm 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를 선택하세요 (배포 전에 몇 가지를 변경할 거예요).
- What would you like to start with? 는
-
프로젝트 디렉터리로 이동하세요.
cd d1-comments-api
2. Hono 설치하기
Workers에서 API를 만들기 위한 가벼운 웹 프레임워크인 Hono ↗︎를 설치하세요.
npm i hono
yarn add hono
pnpm add hono
bun add hono
3. 데이터베이스 만들기
-
Wrangler로 새 D1 데이터베이스를 만드세요.
npx wrangler@latest d1 create d1-comments-api -
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)하기
-
다음 내용으로
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'); -
먼저 로컬 데이터베이스에 스키마를 실행하세요.
npx wrangler d1 execute d1-comments-api --local --file schemas/schema.sql -
로컬에서 테이블이 생성됐는지 확인하세요.
npx wrangler d1 execute d1-comments-api --local --command "SELECT name FROM sqlite_schema WHERE type = 'table'"┌──────────┐ │ name │ ├──────────┤ │ comments │ └──────────┘ -
스키마가 마음에 들면 원격(프로덕션) 데이터베이스에도 적용하세요.
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. 애플리케이션 배포하기
-
Cloudflare 계정에 로그인하세요 (아직 로그인하지 않았다면).
npx wrangler whoami로그인하지 않았다면 Wrangler가 로그인하라고 안내할 거예요.
-
Worker를 배포하세요.
npx wrangler deploy -
댓글을 삽입한 뒤 조회해서 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!"}'Createdcurl 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)
- 사용 가능한 모든 메서드의 전체 목록은 D1 Workers Binding API를 참고하세요.
- 배포하지 않고 데이터베이스를 테스트하는 방법은 D1 로컬 개발을 배워 보세요.
- D1 기반 커뮤니티 프로젝트를 살펴보세요.