본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout
TechProjectsBooksAbout
© 2026 Kreath. All rights reserved.
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 9장: Deno로 REST API 구축하기
2026년 8월 2일·프로그래밍·

9장: Deno로 REST API 구축하기

Deno.serve()를 활용한 HTTP 서버 구축부터 Hono 프레임워크, PostgreSQL 및 Deno KV 데이터베이스 연동, 테스트 작성, 배포까지 REST API 개발 전 과정을 다룹니다.

16분1,525자8개 섹션
typescriptperformancetoolingsecuritydeveloper-experience
공유
deno-runtime9 / 10
12345678910
이전8장: 서버리스와 엣지 배포다음10장: 실전 프로젝트 - Deno 2 풀스택 애플리케이션

REST API 개발의 시작점

이 장에서는 Deno로 실제 프로덕션에서 사용할 수 있는 REST API를 단계별로 구축합니다. 가장 기본적인 Deno.serve()부터 시작하여, Hono 프레임워크 도입, 데이터베이스 연동, 인증, 테스트, 배포까지 전 과정을 다룹니다.

Deno.serve()로 시작하는 HTTP 서버

기본 서버

src/main.ts - 기본 HTTP 서버
typescript
Deno.serve({ port: 8000 }, async (req: Request): Promise<Response> => {
  const url = new URL(req.url);
  const method = req.method;
 
  // 라우팅
  if (url.pathname === "/api/health" && method === "GET") {
    return Response.json({ status: "ok", timestamp: new Date().toISOString() });
  }
 
  if (url.pathname === "/api/echo" && method === "POST") {
    const body = await req.json();
    return Response.json({ echo: body });
  }
 
  return Response.json({ error: "Not Found" }, { status: 404 });
});

Deno.serve()는 웹 표준 Request와 Response를 직접 사용합니다. 추가 라이브러리 없이 바로 HTTP 서버를 구축할 수 있지만, 라우팅이 복잡해지면 프레임워크의 도움이 필요합니다.

수동 라우터 구현

src/router.ts - 간단한 라우터
typescript
type Handler = (req: Request, params: Record<string, string>) => Promise<Response> | Response;
 
interface Route {
  method: string;
  pattern: URLPattern;
  handler: Handler;
}
 
class Router {
  private routes: Route[] = [];
 
  get(path: string, handler: Handler) {
    this.addRoute("GET", path, handler);
  }
 
  post(path: string, handler: Handler) {
    this.addRoute("POST", path, handler);
  }
 
  put(path: string, handler: Handler) {
    this.addRoute("PUT", path, handler);
  }
 
  delete(path: string, handler: Handler) {
    this.addRoute("DELETE", path, handler);
  }
 
  private addRoute(method: string, path: string, handler: Handler) {
    this.routes.push({
      method,
      pattern: new URLPattern({ pathname: path }),
      handler,
    });
  }
 
  async handle(req: Request): Promise<Response> {
    for (const route of this.routes) {
      if (route.method !== req.method) continue;
 
      const match = route.pattern.exec(req.url);
      if (match) {
        const params = match.pathname.groups as Record<string, string>;
        return await route.handler(req, params);
      }
    }
 
    return Response.json({ error: "Not Found" }, { status: 404 });
  }
}
 
export { Router };
Info

URLPattern은 웹 표준 API로, URL 경로의 패턴 매칭을 수행합니다. Deno에서 기본 지원되며, Express의 app.get("/users/:id") 같은 동적 경로 매칭을 가능하게 합니다.

Hono 프레임워크 도입

왜 Hono인가

실제 프로젝트에서는 라우팅, 미들웨어, 에러 처리, 입력 검증 등 다양한 기능이 필요합니다. Hono는 이러한 요구를 충족하면서도 엣지 런타임에 최적화된 경량 웹 프레임워크입니다.

Hono의 특징
text
- 초경량: 코어 사이즈 ~14KB
- 멀티 런타임: Deno, Bun, Cloudflare Workers, Node.js
- TypeScript 기본: 완전한 타입 안전성
- 웹 표준 기반: Request/Response API 사용
- 풍부한 미들웨어: CORS, JWT, Logger, Validator 등

프로젝트 설정

deno.json
json
{
  "imports": {
    "hono": "jsr:@hono/hono@^4",
    "@std/assert": "jsr:@std/assert@^1",
    "zod": "npm:zod@^3.22"
  },
  "tasks": {
    "dev": "deno run --watch --allow-net --allow-env --allow-read=. src/main.ts",
    "test": "deno test --allow-net --allow-env --allow-read=.",
    "check": "deno check src/main.ts"
  }
}

Hono 기반 API 서버

src/main.ts - Hono 서버
typescript
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { prettyJSON } from "hono/pretty-json";
import { userRoutes } from "./routes/users.ts";
import { postRoutes } from "./routes/posts.ts";
 
const app = new Hono();
 
// 전역 미들웨어
app.use("*", logger());
app.use("*", prettyJSON());
app.use(
  "/api/*",
  cors({
    origin: ["http://localhost:3000", "https://myapp.example.com"],
    allowMethods: ["GET", "POST", "PUT", "DELETE"],
    allowHeaders: ["Content-Type", "Authorization"],
  }),
);
 
// 헬스 체크
app.get("/api/health", (c) => {
  return c.json({
    status: "ok",
    version: "1.0.0",
    timestamp: new Date().toISOString(),
  });
});
 
// 라우트 마운트
app.route("/api/users", userRoutes);
app.route("/api/posts", postRoutes);
 
// 404 핸들러
app.notFound((c) => {
  return c.json({ error: "요청한 리소스를 찾을 수 없습니다" }, 404);
});
 
// 전역 에러 핸들러
app.onError((err, c) => {
  console.error(`에러 발생: ${err.message}`);
  return c.json({ error: "내부 서버 오류가 발생했습니다" }, 500);
});
 
Deno.serve({ port: 8000 }, app.fetch);

데이터베이스 연동

Deno KV를 활용한 데이터 계층

간단한 프로젝트나 프로토타이핑에서는 Deno KV만으로도 충분한 데이터 저장소 역할을 합니다.

src/db/kv-store.ts
typescript
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: string;
  updatedAt: string;
}
 
class UserStore {
  private kv: Deno.Kv;
 
  private constructor(kv: Deno.Kv) {
    this.kv = kv;
  }
 
  static async create(): Promise<UserStore> {
    const kv = await Deno.openKv();
    return new UserStore(kv);
  }
 
  async findAll(): Promise<User[]> {
    const users: User[] = [];
    const iter = this.kv.list<User>({ prefix: ["users"] });
    for await (const entry of iter) {
      users.push(entry.value);
    }
    return users;
  }
 
  async findById(id: string): Promise<User | null> {
    const result = await this.kv.get<User>(["users", id]);
    return result.value;
  }
 
  async findByEmail(email: string): Promise<User | null> {
    const result = await this.kv.get<string>(["users_by_email", email]);
    if (!result.value) return null;
    return this.findById(result.value);
  }
 
  async create(data: Omit<User, "id" | "createdAt" | "updatedAt">): Promise<User> {
    const id = crypto.randomUUID();
    const now = new Date().toISOString();
 
    const user: User = {
      id,
      ...data,
      createdAt: now,
      updatedAt: now,
    };
 
    // 이메일 중복 검사를 포함한 원자적 저장
    const existingEmail = await this.kv.get(["users_by_email", user.email]);
    if (existingEmail.value) {
      throw new Error("이미 사용 중인 이메일입니다");
    }
 
    const result = await this.kv.atomic()
      .check(existingEmail) // 이메일 중복 방지
      .set(["users", id], user)
      .set(["users_by_email", user.email], id)
      .commit();
 
    if (!result.ok) {
      throw new Error("사용자 생성 실패 (동시성 충돌)");
    }
 
    return user;
  }
 
  async update(id: string, data: Partial<Pick<User, "name" | "email">>): Promise<User> {
    const existing = await this.kv.get<User>(["users", id]);
    if (!existing.value) {
      throw new Error("사용자를 찾을 수 없습니다");
    }
 
    const updated: User = {
      ...existing.value,
      ...data,
      updatedAt: new Date().toISOString(),
    };
 
    const result = await this.kv.atomic()
      .check(existing)
      .set(["users", id], updated)
      .commit();
 
    if (!result.ok) {
      throw new Error("업데이트 실패 (동시성 충돌)");
    }
 
    return updated;
  }
 
  async delete(id: string): Promise<void> {
    const existing = await this.kv.get<User>(["users", id]);
    if (!existing.value) {
      throw new Error("사용자를 찾을 수 없습니다");
    }
 
    await this.kv.atomic()
      .check(existing)
      .delete(["users", id])
      .delete(["users_by_email", existing.value.email])
      .commit();
  }
}
 
export { UserStore, type User };

PostgreSQL 연동

프로덕션 환경에서 관계형 데이터가 필요하다면 PostgreSQL을 사용합니다.

src/db/postgres.ts
typescript
import { Client } from "npm:pg@8";
 
class Database {
  private client: Client;
 
  constructor() {
    this.client = new Client({
      hostname: Deno.env.get("DB_HOST") ?? "localhost",
      port: Number(Deno.env.get("DB_PORT") ?? 5432),
      user: Deno.env.get("DB_USER") ?? "postgres",
      password: Deno.env.get("DB_PASSWORD") ?? "",
      database: Deno.env.get("DB_NAME") ?? "myapp",
    });
  }
 
  async connect(): Promise<void> {
    await this.client.connect();
  }
 
  async query<T>(sql: string, params?: unknown[]): Promise<T[]> {
    const result = await this.client.queryObject<T>(sql, params);
    return result.rows;
  }
 
  async close(): Promise<void> {
    await this.client.end();
  }
}
 
export { Database };
Warning

PostgreSQL 연동 시 --allow-net 권한에 데이터베이스 호스트를 명시적으로 포함해야 합니다. 예: --allow-net=0.0.0.0:8000,localhost:5432. 또한 --allow-env=DB_HOST,DB_PORT,DB_USER,DB_PASSWORD,DB_NAME으로 필요한 환경 변수만 허용하세요.

라우트와 입력 검증

사용자 API 라우트

src/routes/users.ts
typescript
import { Hono } from "hono";
import { z } from "zod";
import { UserStore } from "../db/kv-store.ts";
 
// 입력 검증 스키마
const CreateUserSchema = z.object({
  name: z.string().min(2, "이름은 2글자 이상이어야 합니다"),
  email: z.string().email("유효한 이메일 주소를 입력하세요"),
});
 
const UpdateUserSchema = z.object({
  name: z.string().min(2).optional(),
  email: z.string().email().optional(),
});
 
const userRoutes = new Hono();
let store: UserStore | null = null;
 
async function getStore(): Promise<UserStore> {
  if (!store) {
    store = await UserStore.create();
  }
  return store;
}
 
// GET /api/users - 사용자 목록 조회
userRoutes.get("/", async (c) => {
  const userStore = await getStore();
  const users = await userStore.findAll();
  return c.json({ users, total: users.length });
});
 
// GET /api/users/:id - 사용자 상세 조회
userRoutes.get("/:id", async (c) => {
  const userStore = await getStore();
  const user = await userStore.findById(c.req.param("id"));
 
  if (!user) {
    return c.json({ error: "사용자를 찾을 수 없습니다" }, 404);
  }
 
  return c.json({ user });
});
 
// POST /api/users - 사용자 생성
userRoutes.post("/", async (c) => {
  const body = await c.req.json();
 
  // Zod로 입력 검증
  const validation = CreateUserSchema.safeParse(body);
  if (!validation.success) {
    return c.json(
      {
        error: "입력 검증 실패",
        details: validation.error.issues,
      },
      400,
    );
  }
 
  try {
    const userStore = await getStore();
    const user = await userStore.create(validation.data);
    return c.json({ user }, 201);
  } catch (error) {
    if (error instanceof Error && error.message.includes("이미 사용 중")) {
      return c.json({ error: error.message }, 409);
    }
    throw error;
  }
});
 
// PUT /api/users/:id - 사용자 수정
userRoutes.put("/:id", async (c) => {
  const body = await c.req.json();
 
  const validation = UpdateUserSchema.safeParse(body);
  if (!validation.success) {
    return c.json({ error: "입력 검증 실패", details: validation.error.issues }, 400);
  }
 
  try {
    const userStore = await getStore();
    const user = await userStore.update(c.req.param("id"), validation.data);
    return c.json({ user });
  } catch (error) {
    if (error instanceof Error && error.message.includes("찾을 수 없")) {
      return c.json({ error: error.message }, 404);
    }
    throw error;
  }
});
 
// DELETE /api/users/:id - 사용자 삭제
userRoutes.delete("/:id", async (c) => {
  try {
    const userStore = await getStore();
    await userStore.delete(c.req.param("id"));
    return c.json({ message: "삭제 완료" });
  } catch (error) {
    if (error instanceof Error && error.message.includes("찾을 수 없")) {
      return c.json({ error: error.message }, 404);
    }
    throw error;
  }
});
 
export { userRoutes };
Tip

Zod를 활용한 입력 검증은 TypeScript의 타입 시스템과 런타임 검증을 연결합니다. 스키마에서 추론된 타입을 사용하면 컴파일 타임과 런타임 모두에서 타입 안전성이 보장됩니다. Hono는 Zod와의 통합 미들웨어인 @hono/zod-validator도 제공합니다.

테스트 작성

API 통합 테스트

src/routes/users_test.ts
typescript
import { assertEquals } from "@std/assert";
 
const BASE_URL = "http://localhost:8000/api";
 
// 테스트용 서버 시작은 별도 프로세스로 관리
// deno task dev 실행 후 테스트 수행
 
Deno.test("GET /api/health - 헬스 체크", async () => {
  const response = await fetch(`${BASE_URL}/health`);
  assertEquals(response.status, 200);
 
  const body = await response.json();
  assertEquals(body.status, "ok");
  assertEquals(typeof body.timestamp, "string");
});
 
Deno.test("POST /api/users - 사용자 생성", async () => {
  const response = await fetch(`${BASE_URL}/users`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      name: "테스트 사용자",
      email: `test-${Date.now()}@example.com`,
    }),
  });
 
  assertEquals(response.status, 201);
  const body = await response.json();
  assertEquals(body.user.name, "테스트 사용자");
  assertEquals(typeof body.user.id, "string");
});
 
Deno.test("POST /api/users - 잘못된 입력 검증", async () => {
  const response = await fetch(`${BASE_URL}/users`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      name: "",
      email: "invalid-email",
    }),
  });
 
  assertEquals(response.status, 400);
  const body = await response.json();
  assertEquals(body.error, "입력 검증 실패");
});
 
Deno.test("GET /api/users/:id - 존재하지 않는 사용자", async () => {
  const response = await fetch(`${BASE_URL}/users/non-existent-id`);
  assertEquals(response.status, 404);
});

단위 테스트

src/db/kv-store_test.ts
typescript
import { assertEquals, assertRejects } from "@std/assert";
import { UserStore } from "./kv-store.ts";
 
Deno.test("UserStore - 사용자 CRUD", async () => {
  const store = await UserStore.create();
 
  // 생성
  const user = await store.create({
    name: "테스트",
    email: `crud-test-${Date.now()}@example.com`,
  });
  assertEquals(user.name, "테스트");
 
  // 조회
  const found = await store.findById(user.id);
  assertEquals(found?.email, user.email);
 
  // 수정
  const updated = await store.update(user.id, { name: "수정됨" });
  assertEquals(updated.name, "수정됨");
 
  // 삭제
  await store.delete(user.id);
  const deleted = await store.findById(user.id);
  assertEquals(deleted, null);
});
 
Deno.test("UserStore - 이메일 중복 검사", async () => {
  const store = await UserStore.create();
  const email = `dup-test-${Date.now()}@example.com`;
 
  await store.create({ name: "첫 번째", email });
 
  await assertRejects(
    () => store.create({ name: "두 번째", email }),
    Error,
    "이미 사용 중인 이메일",
  );
});
테스트 실행
bash
# 모든 테스트 실행
deno test --allow-net --allow-env --allow-read=.
 
# 특정 파일 테스트
deno test --allow-net --allow-env src/db/kv-store_test.ts
 
# 커버리지 포함 실행
deno test --allow-net --allow-env --coverage=./coverage
deno coverage ./coverage

배포

Docker 배포

Dockerfile
dockerfile
FROM denoland/deno:2.0.0
 
WORKDIR /app
 
# 의존성 캐시를 위해 설정 파일 먼저 복사
COPY deno.json deno.lock ./
RUN deno install
 
# 소스 코드 복사
COPY src/ ./src/
 
# 타입 체크
RUN deno check src/main.ts
 
# 최소 권한으로 실행
EXPOSE 8000
CMD ["deno", "run", "--allow-net=0.0.0.0:8000", "--allow-env=DB_HOST,DB_PORT,DB_USER,DB_PASSWORD,DB_NAME", "--allow-read=.", "--no-prompt", "src/main.ts"]

Deno Deploy 배포

Deno Deploy 배포
bash
# CLI 배포
deployctl deploy --project=my-api --prod src/main.ts
 
# 또는 GitHub 연동으로 자동 배포

단일 실행 파일 컴파일

단일 바이너리 컴파일
bash
# 현재 플랫폼용 바이너리 생성
deno compile --allow-net=0.0.0.0:8000 --allow-env --output=my-api src/main.ts
 
# 크로스 컴파일
deno compile --target=x86_64-unknown-linux-gnu --output=my-api-linux src/main.ts
deno compile --target=aarch64-apple-darwin --output=my-api-macos-arm src/main.ts
Info

deno compile은 런타임을 포함한 단일 실행 파일을 생성합니다. 대상 시스템에 Deno가 설치되어 있지 않아도 실행할 수 있으며, 권한 설정도 바이너리에 포함됩니다. Go 언어의 바이너리 배포와 유사한 경험을 제공합니다.

프로젝트 구조 요약

최종 프로젝트 구조
text
my-api/
  src/
    main.ts               # 엔트리포인트
    routes/
      users.ts            # 사용자 API 라우트
      users_test.ts       # 사용자 API 테스트
      posts.ts            # 게시글 API 라우트
    db/
      kv-store.ts         # Deno KV 데이터 계층
      kv-store_test.ts    # KV 스토어 테스트
      postgres.ts         # PostgreSQL 연동 (선택)
    middleware/
      auth.ts             # 인증 미들웨어
      rate-limit.ts       # 요청 제한 미들웨어
  deno.json               # Deno 설정
  deno.lock               # 의존성 잠금 파일
  Dockerfile              # Docker 배포

이 구조를 기반으로 다음 장에서는 Fresh 프론트엔드를 결합한 풀스택 애플리케이션을 구축합니다.

이 글이 도움이 되셨나요?

관련 글

프로그래밍

10장: 실전 프로젝트 - Deno 2 풀스택 애플리케이션

Fresh 프론트엔드와 Deno.serve API, Deno KV를 결합한 풀스택 할 일 관리 애플리케이션을 구축합니다. 인증, 배포, 모니터링까지 실전 프로젝트의 전체 과정을 다룹니다.

2026년 8월 5일·22분
프로그래밍

8장: 서버리스와 엣지 배포

Deno Deploy를 중심으로 서버리스 및 엣지 배포 전략을 다룹니다. 엣지 컴퓨팅 개념, 콜드 스타트 성능, Deno KV를 활용한 엣지 상태 관리, Cloudflare Workers 호환성을 분석합니다.

2026년 7월 30일·19분
프로그래밍

7장: Fresh 프레임워크 - Deno 네이티브 웹 개발

Deno의 공식 웹 프레임워크 Fresh를 심층 분석합니다. Islands Architecture, Preact 기반 컴포넌트, 라우팅, 미들웨어, 데이터 페칭 등 핵심 기능을 다룹니다.

2026년 7월 28일·16분
이전 글8장: 서버리스와 엣지 배포
다음 글10장: 실전 프로젝트 - Deno 2 풀스택 애플리케이션

댓글

목차

약 16분 남음
  • REST API 개발의 시작점
  • Deno.serve()로 시작하는 HTTP 서버
    • 기본 서버
    • 수동 라우터 구현
  • Hono 프레임워크 도입
    • 왜 Hono인가
    • 프로젝트 설정
    • Hono 기반 API 서버
  • 데이터베이스 연동
    • Deno KV를 활용한 데이터 계층
    • PostgreSQL 연동
  • 라우트와 입력 검증
    • 사용자 API 라우트
  • 테스트 작성
    • API 통합 테스트
    • 단위 테스트
  • 배포
    • Docker 배포
    • Deno Deploy 배포
    • 단일 실행 파일 컴파일
  • 프로젝트 구조 요약