📚 初心者から実践活用まで完全網羅 — ClaudeCodeを今日から使いこなせる

実践活用術

ClaudeCodeでAPIサーバーを構築する方法【Node.js / Express実践】

ClaudeCodeを使ってNode.jsとExpressでREST APIサーバーを構築する手順を実践的に解説します。エンドポイント設計、データベース連携、認証、デプロイまで網羅した完全ガイドです。

2026-05-11·約16分で読める·#ClaudeCode#Node.js#Express
[ Advertisement ]

ClaudeCodeでAPIサーバーを構築する方法【Node.js / Express実践】

Webアプリやスマホアプリの裏側で動くAPIサーバー。一度自分で作ってみたいと思いつつ、「Expressのルーティングが難しい」「データベース連携で挫折した」「認証ってどうするの?」と立ち止まってしまう方は少なくありません。本記事ではClaudeCodeを活用しながら、Node.jsとExpressでシンプルなタスク管理APIを実際に構築するプロセスを解説します。エンドポイント設計、データベース連携、認証、デプロイまでを1本でまとめていますので、ぜひ手を動かしながら読み進めてください。完成する頃にはAPIサーバーの全体像が腑に落ちているはずです。

結論:ClaudeCodeを使えばAPIサーバーは「設計→実装→デプロイ」が一気通貫で進む

ClaudeCodeとExpressの組み合わせは、APIサーバー開発の学習曲線を一気に滑らかにします。具体的には次のような流れで進められます。最初に「タスク管理APIを作りたい」と相談すると、ClaudeCodeはエンドポイントの一覧(例:GET /tasksPOST /tasksPUT /tasks/:idDELETE /tasks/:id)を提案してくれます。次に、それぞれに対応するルーター・コントローラ・サービス層を分割した設計を提示し、コードを生成します。データベースはSQLite、PostgreSQL、Cloudflare D1など状況に応じて選べます。さらに、JWTやセッション認証の実装、バリデーション、エラーハンドリング、ロギング、テストコードの生成までシームレスに進みます。最終的にはCloudflare WorkersやRailwayなどへデプロイし、本番運用に乗せるところまで到達できます。

特に強調したいのは、ClaudeCodeが「動くだけのコード」ではなく「保守しやすい構成」を提案してくれる点です。レイヤード設計、入力検証、エラー応答の統一など、プロが当たり前に行う設計指針が標準で盛り込まれます。本記事ではこの恩恵をフルに使って、現場で通用するAPIサーバーを完成させていきます。

h2-1. プロジェクトのセットアップとディレクトリ構成

まずは作業フォルダを作りClaudeCodeを起動します。

mkdir task-api && cd task-api
claude

最初の指示はこちらです。

プロンプト例1:「Node.js + Express + TypeScriptでREST APIサーバーを作りたいです。タスク管理用のAPIで、データベースはまずSQLite(better-sqlite3)を使います。ts-nodeで開発できるように初期化してください。」

ClaudeCodeはpackage.jsonを生成し、必要な依存関係をインストールします。出来上がる構成はこのようなイメージです。

task-api/
├── src/
│   ├── routes/
│   │   └── tasks.ts
│   ├── controllers/
│   │   └── tasksController.ts
│   ├── services/
│   │   └── tasksService.ts
│   ├── db/
│   │   ├── client.ts
│   │   └── schema.sql
│   ├── middleware/
│   │   ├── errorHandler.ts
│   │   └── auth.ts
│   ├── types/
│   │   └── task.ts
│   └── server.ts
├── tsconfig.json
└── package.json

ルートとコントローラ、サービスを分けることで、テストしやすく変更にも強い構造になります。

h2-2. データベース設計とマイグレーション

タスク管理APIのDBスキーマを設計します。最小構成では次のテーブルがあれば十分です。

-- src/db/schema.sql
CREATE TABLE IF NOT EXISTS tasks (
  id TEXT PRIMARY KEY,
  title TEXT NOT NULL,
  description TEXT,
  status TEXT NOT NULL DEFAULT 'todo',
  due_date TEXT,
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS users (
  id TEXT PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

プロンプト例2:「上のスキーマを使って、better-sqlite3でDB接続を初期化するモジュールをsrc/db/client.tsに作成してください。アプリ起動時にスキーマが未作成なら自動で作るようにしてください。」

// src/db/client.ts
import Database from 'better-sqlite3';
import fs from 'fs';
import path from 'path';

const db = new Database(path.join(process.cwd(), 'data.sqlite'));
const schema = fs.readFileSync(path.join(__dirname, 'schema.sql'), 'utf8');
db.exec(schema);

export default db;

将来的にPostgreSQLや本番DBへ移行することを見越して、DBアクセスはサービス層に集約しておきましょう。

h2-3. ルーティングとコントローラの実装

Expressのルーティングはsrc/routes/tasks.tsに集約します。

// src/routes/tasks.ts
import { Router } from 'express';
import * as ctrl from '../controllers/tasksController';

const router = Router();
router.get('/', ctrl.list);
router.get('/:id', ctrl.get);
router.post('/', ctrl.create);
router.put('/:id', ctrl.update);
router.delete('/:id', ctrl.remove);

export default router;

コントローラは受け取ったリクエストを検証し、サービスに処理を委譲します。

プロンプト例3:「tasksController.tsを作成してください。zodで入力バリデーションし、サービス層を呼び出した結果を返却してください。エラーは次のミドルウェアに渡してください。」

// src/controllers/tasksController.ts
import { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import * as service from '../services/tasksService';

const createSchema = z.object({
  title: z.string().min(1),
  description: z.string().optional(),
  status: z.enum(['todo', 'doing', 'done']).optional(),
  due_date: z.string().optional(),
});

export async function list(req: Request, res: Response, next: NextFunction) {
  try {
    res.json(await service.listTasks());
  } catch (e) { next(e); }
}

export async function get(req: Request, res: Response, next: NextFunction) {
  try {
    const task = await service.getTask(req.params.id);
    if (!task) return res.status(404).json({ error: 'Not Found' });
    res.json(task);
  } catch (e) { next(e); }
}

export async function create(req: Request, res: Response, next: NextFunction) {
  try {
    const data = createSchema.parse(req.body);
    const task = await service.createTask(data);
    res.status(201).json(task);
  } catch (e) { next(e); }
}

export async function update(req: Request, res: Response, next: NextFunction) {
  try {
    const data = createSchema.partial().parse(req.body);
    const task = await service.updateTask(req.params.id, data);
    if (!task) return res.status(404).json({ error: 'Not Found' });
    res.json(task);
  } catch (e) { next(e); }
}

export async function remove(req: Request, res: Response, next: NextFunction) {
  try {
    const ok = await service.deleteTask(req.params.id);
    if (!ok) return res.status(404).json({ error: 'Not Found' });
    res.status(204).end();
  } catch (e) { next(e); }
}

サービス層は次のように、DB操作だけに専念させます。

// src/services/tasksService.ts
import db from '../db/client';
import { randomUUID } from 'crypto';

export type TaskInput = {
  title: string;
  description?: string;
  status?: 'todo' | 'doing' | 'done';
  due_date?: string;
};

export async function listTasks() {
  return db.prepare('SELECT * FROM tasks ORDER BY created_at DESC').all();
}

export async function getTask(id: string) {
  return db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
}

export async function createTask(data: TaskInput) {
  const id = randomUUID();
  db.prepare(
    `INSERT INTO tasks (id, title, description, status, due_date)
     VALUES (?, ?, ?, ?, ?)`
  ).run(id, data.title, data.description ?? null, data.status ?? 'todo', data.due_date ?? null);
  return getTask(id);
}

export async function updateTask(id: string, data: Partial<TaskInput>) {
  const current = await getTask(id);
  if (!current) return null;
  const merged = { ...current, ...data, updated_at: new Date().toISOString() };
  db.prepare(
    `UPDATE tasks SET title=?, description=?, status=?, due_date=?, updated_at=? WHERE id=?`
  ).run(merged.title, merged.description, merged.status, merged.due_date, merged.updated_at, id);
  return getTask(id);
}

export async function deleteTask(id: string) {
  const r = db.prepare('DELETE FROM tasks WHERE id = ?').run(id);
  return r.changes > 0;
}

h2-4. エラーハンドリングとロギング

エラー処理を毎回コントローラに書くと冗長になるため、共通ミドルウェアにまとめます。

プロンプト例4:「Zodのバリデーションエラーは400で返し、それ以外の例外は500で返す共通エラーハンドラを作成してください。本番では詳細なエラー内容をクライアントに返さない設計にしてください。」

// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { ZodError } from 'zod';

export function errorHandler(err: any, _req: Request, res: Response, _next: NextFunction) {
  if (err instanceof ZodError) {
    return res.status(400).json({ error: 'ValidationError', details: err.flatten() });
  }
  console.error(err);
  const message = process.env.NODE_ENV === 'production' ? 'Internal Server Error' : err.message;
  res.status(500).json({ error: message });
}

ロギングはmorganpinoを使うとよいです。アクセスログを残しておくと、本番でのトラブルシューティングが格段に楽になります。

h2-5. 認証:JWTとCloudflare Accessの使い分け

API認証の方式は用途によって変わります。社内利用や限定公開ならCloudflare Accessが圧倒的におすすめです。Cloudflare Accessはアプリ側で認証ロジックを書かずに、ZeroTrustダッシュボードで「特定メールアドレスのみ許可」「IPアドレスで制限」などを設定できます。独自の認証を書くより安全で、メンテナンスもほぼ不要です。

一方、不特定多数のクライアントから利用される一般公開APIではJWTが標準です。

プロンプト例5:「JWTを使ったログインAPIを実装してください。POST /auth/login でメールとパスワードを受け取り、bcryptで照合した上でJWTを返します。トークン検証ミドルウェアも作成してください。」

// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET!;

export function requireAuth(req: Request, res: Response, next: NextFunction) {
  const header = req.headers.authorization;
  if (!header?.startsWith('Bearer ')) return res.status(401).json({ error: 'Unauthorized' });
  try {
    const payload = jwt.verify(header.slice(7), SECRET);
    (req as any).user = payload;
    next();
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
}

ログインエンドポイントは次のようになります。

// src/routes/auth.ts
import { Router } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import db from '../db/client';

const router = Router();
router.post('/login', async (req, res, next) => {
  try {
    const { email, password } = req.body;
    const user: any = db.prepare('SELECT * FROM users WHERE email = ?').get(email);
    if (!user) return res.status(401).json({ error: 'Invalid credentials' });
    const ok = await bcrypt.compare(password, user.password_hash);
    if (!ok) return res.status(401).json({ error: 'Invalid credentials' });
    const token = jwt.sign({ sub: user.id }, process.env.JWT_SECRET!, { expiresIn: '7d' });
    res.json({ token });
  } catch (e) { next(e); }
});
export default router;

注意:APIキーやJWTシークレットは絶対にソースコードに直書きせず、環境変数で管理してください。Cloudflareにデプロイする場合はwrangler secret putコマンドでSecretsに登録するのが鉄則です。

[ Advertisement ]

h2-6. サーバー起動とアプリの組み立て

src/server.tsで全体を組み立てます。

// src/server.ts
import express from 'express';
import tasksRouter from './routes/tasks';
import authRouter from './routes/auth';
import { errorHandler } from './middleware/errorHandler';
import { requireAuth } from './middleware/auth';

const app = express();
app.use(express.json());
app.use('/auth', authRouter);
app.use('/tasks', requireAuth, tasksRouter);
app.use(errorHandler);

const port = Number(process.env.PORT) || 3000;
app.listen(port, () => console.log(`API listening on :${port}`));

開発時はnpx ts-node-dev src/server.tsで起動できます。curlやPostman、VSCodeのREST Client拡張などで動作確認してみましょう。

curl -X POST http://localhost:3000/tasks \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"title":"資料作成","status":"todo"}'

h2-7. テストコードの作成

APIサーバーこそテストが効きます。supertestを使うと、Expressアプリを起動せずにエンドポイントを叩いた結果を検証できます。

プロンプト例6:「Jest と supertest を導入し、/tasks の作成・取得・削除をテストするコードを作成してください。テスト用DBは別ファイルに切り替えてください。」

// tests/tasks.test.ts
import request from 'supertest';
import app from '../src/app'; // server.tsからexpressアプリだけexportしておく

describe('Tasks API', () => {
  let id: string;
  it('creates a task', async () => {
    const res = await request(app)
      .post('/tasks')
      .set('Authorization', `Bearer ${process.env.TEST_TOKEN}`)
      .send({ title: 'テスト' });
    expect(res.status).toBe(201);
    id = res.body.id;
  });
  it('gets the task', async () => {
    const res = await request(app)
      .get(`/tasks/${id}`)
      .set('Authorization', `Bearer ${process.env.TEST_TOKEN}`);
    expect(res.status).toBe(200);
    expect(res.body.title).toBe('テスト');
  });
});

テストを資産として残しておけば、リファクタリングや機能追加でも壊れにくいAPIに育っていきます。

h2-8. デプロイ:Cloudflare Workersへの本番公開

Node.js / ExpressのAPIをCloudflare Workersに乗せる場合は、honoへの移植か、@cloudflare/workers-typesと互換レイヤーを使う方法があります。本格運用するなら、最初からHonoで書くか、ExpressアプリをHonoに置き換える形が現実的です。

プロンプト例7:「このExpressのAPIをHonoに移植してCloudflare Workersで動くようにしてください。DBはCloudflare D1に切り替え、wrangler.jsoncでD1バインディングを設定してください。preview_urlsはfalseにしてください。」

wrangler.jsoncのサンプルです。

{
  "name": "task-api",
  "main": "src/worker.ts",
  "compatibility_date": "2026-05-01",
  "preview_urls": false,
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "task-api-db",
      "database_id": "<your-d1-id>"
    }
  ]
}

シークレットは次のコマンドで登録します。

wrangler secret put JWT_SECRET

デプロイはwrangler deployの一発です。Cloudflare Accessでメール制限をかければ、社内向けAPIとして安全に運用できます。

FAQ

Q1. なぜサービス層とコントローラを分けるのですか? A. ビジネスロジックとHTTP層を分けることで、後からCLIやバッチからも同じロジックを再利用できるからです。テストも書きやすくなります。

Q2. SQLiteと PostgreSQLどちらを選ぶべきですか? A. 個人プロジェクトや学習用途ならSQLiteで十分です。マルチユーザー・本番運用ならPostgreSQL、Cloudflare上で動かすならD1がベストです。

Q3. APIキーはどう管理すればよいですか? A. 必ず環境変数に入れ、本番ではCloudflareのSecrets機能を使います。ソースコードに直書きするのは絶対NGです。

Q4. CORS対応はどうすればよいですか? A. corsミドルウェアを使い、許可するオリジンをホワイトリスト方式で指定するのが安全です。

Q5. ファイルアップロードはどう実装しますか? A. multerなどのミドルウェアを使ってマルチパートを処理し、ファイル本体はR2やS3に保存するのが一般的です。

Q6. レートリミットは必要ですか? A. 公開APIなら必須です。express-rate-limitやCloudflare WAFのRate Limitingルールを併用すると安心です。

Q7. ドキュメントはどう用意しますか? A. OpenAPI(Swagger)仕様をClaudeCodeに書かせて、swagger-ui-expressで表示するのが定番です。

まとめ

APIサーバー開発は、設計判断が多くて初心者がつまずきやすい領域ですが、ClaudeCodeを伴走させればそのハードルは大きく下がります。本記事ではタスク管理APIを題材に、ディレクトリ設計、DB連携、コントローラ・サービスの分離、認証、テスト、Cloudflareデプロイまでを一気通貫で実装しました。設計の意図を理解しながら進めることで、フレームワークや言語が変わっても応用が利くスキルが身につきます。次は自分のアイデアを形にして、実際に動くAPIを世の中に出してみてください。

関連記事

[ Advertisement ]

この記事をシェア

Related Articles

あわせて読みたい記事