mcpField
N
nodejs-backend

nodejs-backend

github.com/alinaqi開発711434 回インストール2 週間前に更新
リポジトリ

nodejs-backend について

保守しやすいサーバーアプリケーションを構築する開発者向けの、Node.js バックエンド設計ガイドです。API設計、データアクセス、設定、エラー処理、テストを扱います。

主な機能

  1. 01レイヤー別のプロジェクト構成
  2. 02Express のルートハンドラーパターン
  3. 03コンポジションルートでの依存性注入
  4. 04ドメインおよび全体のエラー処理
  5. 05リポジトリとトランザクションのパターン
  6. 06設定の検証とテスト

活用シーン

  1. 01Node.js サービス構成の設計
  2. 02検証付き API ルートの追加
  3. 03データベースアクセスとトランザクションの整理
  4. 04ユニットテストと統合テストの計画

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/alinaqi/maggy --skill nodejs-backend

インストール後、Claude Code で/nodejs-backendを入力して読み込まれたか確認します。

詳細

Node.js バックエンドスキル


プロジェクト構成

project/
├── src/
│   ├── core/                   # Pure business logic
│   │   ├── types.ts            # Domain types
│   │   ├── errors.ts           # Domain errors
│   │   └── services/           # Pure functions
│   │       ├── user.ts
│   │       └── order.ts
│   ├── infra/                  # Side effects
│   │   ├── http/               # HTTP layer
│   │   │   ├── server.ts       # Server setup
│   │   │   ├── routes/         # Route handlers
│   │   │   └── middleware/     # Express middleware
│   │   ├── db/                 # Database
│   │   │   ├── client.ts       # DB connection
│   │   │   ├── repositories/   # Data access
│   │   │   └── migrations/     # Schema migrations
│   │   └── external/           # Third-party APIs
│   ├── config/                 # Configuration
│   │   └── index.ts            # Env vars, validated
│   └── index.ts                # Entry point
├── tests/
│   ├── unit/
│   └── integration/
├── package.json
└── CLAUDE.md

API 設計

ルートハンドラーパターン
// routes/users.ts
import { Router } from 'express';
import { z } from 'zod';
import { createUser } from '../../core/services/user';
import { UserRepository } from '../db/repositories/user';

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
});

export function createUserRoutes(userRepo: UserRepository): Router {
  const router = Router();

  router.post('/', async (req, res, next) => {
    try {
      const input = CreateUserSchema.parse(req.body);
      const user = await createUser(input, userRepo);
      res.status(201).json(user);
    } catch (error) {
      next(error);
    }
  });

  return router;
}
コンポジションルートでの依存性注入
// index.ts
import { createApp } from './infra/http/server';
import { createDbClient } from './infra/db/client';
import { UserRepository } from './infra/db/repositories/user';
import { createUserRoutes } from './infra/http/routes/users';

async function main(): Promise<void> {
  const db = await createDbClient();
  const userRepo = new UserRepository(db);
  
  const app = createApp({
    userRoutes: createUserRoutes(userRepo),
  });
  
  app.listen(3000);
}

エラー処理

ドメインエラー
// core/errors.ts
export class DomainError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 400
  ) {
    super(message);
    this.name = 'DomainError';
  }
}

export class NotFoundError extends DomainError {
  constructor(resource: string, id: string) {
    super(`${resource} with id ${id} not found`, 'NOT_FOUND', 404);
  }
}

export class ValidationError extends DomainError {
  constructor(message: string) {
    super(message, 'VALIDATION_ERROR', 400);
  }
}
グローバルエラーハンドラー
// middleware/errorHandler.ts
import { ErrorRequestHandler } from 'express';
import { DomainError } from '../../core/errors';
import { ZodError } from 'zod';

export const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
  if (err instanceof DomainError) {
    return res.status(err.statusCode).json({
      error: { code: err.code, message: err.message },
    });
  }

  if (err instanceof ZodError) {
    return res.status(400).json({
      error: { code: 'VALIDATION_ERROR', details: err.errors },
    });
  }

  console.error('Unexpected error:', err);
  return res.status(500).json({
    error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' },
  });
};

データベースパターン

リポジトリパターン
// db/repositories/user.ts
import { Kysely } from 'kysely';
import { Database, User } from '../types';

export class UserRepository {
  constructor(private db: Kysely<Database>) {}

  async findById(id: string): Promise<User | null> {
    return this.db
      .selectFrom('users')
      .where('id', '=', id)
      .selectAll()
      .executeTakeFirst() ?? null;
  }

  async create(data: Omit<User, 'id' | 'createdAt'>): Promise<User> {
    return this.db
      .insertInto('users')
      .values(data)
      .returningAll()
      .executeTakeFirstOrThrow();
  }
}
トランザクション
async function transferFunds(
  fromId: string,
  toId: string,
  amount: number,
  db: Kysely<Database>
): Promise<void> {
  await db.transaction().execute(async (trx) => {
    await trx
      .updateTable('accounts')
      .set((eb) => ({ balance: eb('balance', '-', amount) }))
      .where('id', '=', fromId)
      .execute();

    await trx
      .updateTable('accounts')
      .set((eb) => ({ balance: eb('balance', '+', amount) }))
      .where('id', '=', toId)
      .execute();
  });
}

設定

検証済み設定
// config/index.ts
import { z } from 'zod';

const ConfigSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(1),
});

export type Config = z.infer<typeof ConfigSchema>;

export function loadConfig(): Config {
  return ConfigSchema.parse(process.env);
}

テスト

ユニットテスト(コア)
// tests/unit/services/user.test.ts
import { createUser } from '../../../src/core/services/user';

describe('createUser', () => {
  it('creates user with valid data', async () => {
    const mockRepo = {
      create: jest.fn().mockResolvedValue({ id: '1', email: '[email protected]' }),
      findByEmail: jest.fn().mockResolvedValue(null),
    };

    const result = await createUser({ email: '[email protected]', name: 'Test' }, mockRepo);

    expect(result.email).toBe('[email protected]');
    expect(mockRepo.create).toHaveBeenCalledTimes(1);
  });
});
統合テスト(API)
// tests/integration/users.test.ts
import request from 'supertest';
import { createTestApp, createTestDb } from '../helpers';

describe('POST /users', () => {
  let app: Express;
  let db: TestDb;

  beforeAll(async () => {
    db = await createTestDb();
    app = createTestApp(db);
  });

  afterAll(async () => {
    await db.destroy();
  });

  it('creates user and returns 201', async () => {
    const response = await request(app)
      .post('/users')
      .send({ email: '[email protected]', name: 'New User' });

    expect(response.status).toBe(201);
    expect(response.body.email).toBe('[email protected]');
  });
});

Node.js のアンチパターン

  • ❌ コールバック地獄 — async/await を使用します
  • ❌ Promise の未処理の拒否 — 必ず捕捉するか、エラーハンドラーに捕捉させます
  • ❌ イベントループのブロッキング — 負荷の高い計算は別に処理します
  • ❌ コード内のシークレット — 環境変数を使用します
  • ❌ SQL 文字列の連結 — パラメーター化クエリを使用します
  • ❌ 入力検証がない — API の境界で検証します
  • ❌ 本番環境での Console.log — 適切なロガーを使用します
  • ❌ グレースフルシャットダウンがない — SIGTERM を処理します
  • ❌ モノリシックなルートファイル — リソースごとに分割します

コメント