Fastify+TypeORMで作る"ノーコード連携"API ― Difyを繋いでDX即戦力化

プログラミング

投稿日:2025/06/16 11:45

更新日:2025/06/16 11:45

Fastify+TypeORMで作る"ノーコード連携"API ― Difyを繋いでDX即戦力化

Fastify+TypeORMで作る"ノーコード連携"API ― Difyを繋いでDX即戦力化

はじめに:DXプロジェクトでのAPI開発の課題

現代のDX(デジタルトランスフォーメーション)プロジェクトにおいて、API開発は中核的な役割を担っています。2024年の調査によると、全世界のトップビジネスリーダーの74%がデジタルイニシアチブを最優先事項と位置づけており、デジタル変革支出は2027年までに3.9兆ドルに達すると予測されています。

しかし、従来のAPI開発には深刻な課題があります。プロトタイプから本格実装まで長い時間を要し、パフォーマンス要件との兼ね合いで技術選定に悩み、さらに非技術者との連携が困難になりがちです。特にAI機能の統合は複雑で、ノーコード/ローコードツールとの連携需要が高まる中、従来の開発手法では対応しきれない状況が生まれています。

本記事では、これらの課題を解決するFastify + TypeORM + Difyの技術スタックを用いた実装方法を、実際のコード例とともに詳しく解説します。

Fastify + TypeORMの技術選定理由

Fastifyが選ばれる理由

Fastifyは、Node.jsエコシステムで最高水準のパフォーマンスを提供するWebフレームワークです。最大76,000+リクエスト/秒の処理能力を誇り、Express.jsの約2倍の処理速度を実現します。

typescript
// Fastifyの基本セットアップ
import fastify from 'fastify';

const app = fastify({
  logger: {
    level: 'info',
    prettyPrint: process.env.NODE_ENV === 'development'
  }
});

// プラグインシステムによる拡張
await app.register(require('@fastify/cors'), {
  origin: true
});

await app.register(require('@fastify/jwt'), {
  secret: process.env.JWT_SECRET
});

非同期処理とイベント駆動アーキテクチャにより高負荷環境でも優れたパフォーマンスを発揮し、TypeScriptサポートも充実しているため、型安全性を確保しながら開発効率を向上させることができます。

TypeORMの技術的優位性

TypeORMは、TypeScriptとJavaScript用の最も人気の高いORMです。DataMapperとActiveRecordパターンの両方をサポートし、開発チームの要件に応じた柔軟な選択が可能です。

typescript
// エンティティ定義
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  email: string;

  @Column()
  name: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

@Entity()
export class Conversation {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  userId: number;

  @Column('text')
  message: string;

  @Column('text')
  response: string;

  @Column('json', { nullable: true })
  context: Record<string, any>;

  @CreateDateColumn()
  createdAt: Date;
}

このように、TypeScriptの型システムを活用した型安全なデータベース操作が可能で、コンパイル時にエラーを検出できるため、本番環境での不具合を大幅に削減できます。

プロジェクト構成とセットアップ

効率的な開発のために、以下のプロジェクト構造を採用します:

fastify-typeorm-dify-api/
├── src/
│   ├── entities/          # TypeORMエンティティ
│   ├── repositories/      # データアクセス層
│   ├── services/          # ビジネスロジック
│   ├── controllers/       # API エンドポイント
│   ├── middleware/        # 認証・ログ等
│   ├── config/           # 設定ファイル
│   └── app.ts            # アプリケーションエントリポイント
├── tests/                # テストファイル
├── docker-compose.yml    # 開発環境
└── Dockerfile           # 本番環境

依存関係のセットアップ

bash
# プロジェクト初期化
npm init -y
npm install fastify @fastify/cors @fastify/jwt @fastify/helmet
npm install typeorm pg reflect-metadata
npm install @types/node typescript ts-node nodemon
npm install pino pino-pretty

Docker環境の構築

yaml
# docker-compose.yml
version: '3.8'
services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: dify_api
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://postgres:password@postgres:5432/dify_api
      REDIS_URL: redis://redis:6379
    depends_on:
      - postgres
      - redis

volumes:
  postgres_data:

RESTful API実装の実践

データアクセス層の実装

typescript
// src/repositories/UserRepository.ts
import { Repository } from 'typeorm';
import { User } from '../entities/User';
import { AppDataSource } from '../config/database';

export class UserRepository {
  private repository: Repository<User>;

  constructor() {
    this.repository = AppDataSource.getRepository(User);
  }

  async findById(id: number): Promise<User | null> {
    return this.repository.findOne({ where: { id } });
  }

  async findByEmail(email: string): Promise<User | null> {
    return this.repository.findOne({ where: { email } });
  }

  async create(userData: Partial<User>): Promise<User> {
    const user = this.repository.create(userData);
    return this.repository.save(user);
  }

  async update(id: number, userData: Partial<User>): Promise<User | null> {
    await this.repository.update(id, userData);
    return this.findById(id);
  }
}

サービス層の実装

typescript
// src/services/UserService.ts
import { UserRepository } from '../repositories/UserRepository';
import { User } from '../entities/User';

export class UserService {
  private userRepository: UserRepository;

  constructor() {
    this.userRepository = new UserRepository();
  }

  async createUser(email: string, name: string): Promise<User> {
    // メールアドレスの重複チェック
    const existingUser = await this.userRepository.findByEmail(email);
    if (existingUser) {
      throw new Error('User with this email already exists');
    }

    return this.userRepository.create({ email, name });
  }

  async getUserById(id: number): Promise<User | null> {
    return this.userRepository.findById(id);
  }

  async updateUser(id: number, updates: Partial<User>): Promise<User | null> {
    return this.userRepository.update(id, updates);
  }
}

Fastifyコントローラーの実装

typescript
// src/controllers/userController.ts
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { UserService } from '../services/UserService';

const userService = new UserService();

export async function userRoutes(fastify: FastifyInstance) {
  // スキーマ定義
  const createUserSchema = {
    body: {
      type: 'object',
      required: ['email', 'name'],
      properties: {
        email: { type: 'string', format: 'email' },
        name: { type: 'string', minLength: 1, maxLength: 100 }
      }
    },
    response: {
      201: {
        type: 'object',
        properties: {
          id: { type: 'number' },
          email: { type: 'string' },
          name: { type: 'string' },
          createdAt: { type: 'string' },
          updatedAt: { type: 'string' }
        }
      }
    }
  };

  // ユーザー作成
  fastify.post('/users', { schema: createUserSchema }, async (request: FastifyRequest, reply: FastifyReply) => {
    try {
      const { email, name } = request.body as { email: string; name: string };
      const user = await userService.createUser(email, name);
      return reply.status(201).send(user);
    } catch (error) {
      fastify.log.error(error);
      return reply.status(400).send({
        error: 'Bad Request',
        message: error.message
      });
    }
  });

  // ユーザー取得
  fastify.get('/users/:id', async (request: FastifyRequest, reply: FastifyReply) => {
    try {
      const { id } = request.params as { id: string };
      const user = await userService.getUserById(parseInt(id));
      
      if (!user) {
        return reply.status(404).send({
          error: 'Not Found',
          message: 'User not found'
        });
      }

      return reply.send(user);
    } catch (error) {
      fastify.log.error(error);
      return reply.status(500).send({
        error: 'Internal Server Error'
      });
    }
  });
}

Difyとの連携実装

Difyは、ビジュアルワークフローでAIアプリケーションを構築できるプラットフォームです。APIを通じて既存システムと簡単に統合できます。

Dify APIクライアントの実装

typescript
// src/services/DifyService.ts
import fetch from 'node-fetch';

interface DifyResponse {
  answer: string;
  conversation_id: string;
  message_id: string;
  created_at: number;
}

interface DifyRequest {
  inputs: Record<string, any>;
  query: string;
  response_mode: 'blocking' | 'streaming';
  user: string;
  conversation_id?: string;
}

export class DifyService {
  private baseURL: string;
  private apiKey: string;

  constructor(baseURL: string, apiKey: string) {
    this.baseURL = baseURL;
    this.apiKey = apiKey;
  }

  async chatCompletion(message: string, userId: string, conversationId?: string): Promise<DifyResponse> {
    const requestData: DifyRequest = {
      inputs: {},
      query: message,
      response_mode: 'blocking',
      user: userId.toString(),
      ...(conversationId && { conversation_id: conversationId })
    };

    const response = await fetch(`${this.baseURL}/chat-messages`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(requestData)
    });

    if (!response.ok) {
      throw new Error(`Dify API error: ${response.status} ${response.statusText}`);
    }

    return response.json() as Promise<DifyResponse>;
  }

  async healthCheck(): Promise<boolean> {
    try {
      const response = await fetch(`${this.baseURL}/parameters`, {
        headers: {
          'Authorization': `Bearer ${this.apiKey}`
        }
      });
      return response.ok;
    } catch {
      return false;
    }
  }
}

AI機能統合エンドポイント

typescript
// src/controllers/aiController.ts
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { DifyService } from '../services/DifyService';
import { ConversationService } from '../services/ConversationService';

const difyService = new DifyService(
  process.env.DIFY_API_URL!,
  process.env.DIFY_API_KEY!
);
const conversationService = new ConversationService();

export async function aiRoutes(fastify: FastifyInstance) {
  // 認証が必要なルートグループ
  await fastify.register(async function (fastify) {
    // JWT認証フック
    fastify.addHook('onRequest', async (request, reply) => {
      try {
        await request.jwtVerify();
      } catch (err) {
        reply.send(err);
      }
    });

    // AI チャットエンドポイント
    fastify.post('/ai/chat', {
      schema: {
        body: {
          type: 'object',
          required: ['message'],
          properties: {
            message: { type: 'string', minLength: 1, maxLength: 1000 },
            conversation_id: { type: 'string' },
            context: { type: 'object' }
          }
        },
        response: {
          200: {
            type: 'object',
            properties: {
              answer: { type: 'string' },
              conversation_id: { type: 'string' },
              message_id: { type: 'string' }
            }
          }
        }
      }
    }, async (request: FastifyRequest, reply: FastifyReply) => {
      try {
        const { message, conversation_id, context } = request.body as {
          message: string;
          conversation_id?: string;
          context?: Record<string, any>;
        };
        const user = request.user as { id: number };

        // Dify AIに問い合わせ
        const aiResponse = await difyService.chatCompletion(
          message,
          user.id.toString(),
          conversation_id
        );

        // 会話履歴をデータベースに保存
        await conversationService.saveConversation({
          userId: user.id,
          message,
          response: aiResponse.answer,
          context,
          conversationId: aiResponse.conversation_id
        });

        return reply.send({
          answer: aiResponse.answer,
          conversation_id: aiResponse.conversation_id,
          message_id: aiResponse.message_id
        });
      } catch (error) {
        fastify.log.error('AI Chat Error:', error);
        
        if (error.message.includes('Dify API error')) {
          return reply.status(502).send({
            error: 'AI Service Error',
            message: 'AI service is temporarily unavailable'
          });
        }

        return reply.status(500).send({
          error: 'Internal Server Error'
        });
      }
    });

    // 会話履歴取得
    fastify.get('/ai/conversations', async (request: FastifyRequest, reply: FastifyReply) => {
      try {
        const user = request.user as { id: number };
        const conversations = await conversationService.getUserConversations(user.id);
        return reply.send(conversations);
      } catch (error) {
        fastify.log.error('Get Conversations Error:', error);
        return reply.status(500).send({
          error: 'Internal Server Error'
        });
      }
    });
  });
}

パフォーマンス最適化とセキュリティ

キャッシュ戦略の実装

typescript
// src/middleware/cache.ts
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

export function createCacheMiddleware(ttl: number = 300) {
  return async (request: any, reply: any) => {
    if (request.method !== 'GET') return;

    const cacheKey = `cache:${request.url}`;
    const cached = await redis.get(cacheKey);

    if (cached) {
      return reply
        .header('X-Cache', 'HIT')
        .send(JSON.parse(cached));
    }

    // レスポンスをキャッシュするフック
    reply.hijack();
    const originalSend = reply.send;
    
    reply.send = function(payload: any) {
      redis.setex(cacheKey, ttl, JSON.stringify(payload));
      return originalSend.call(this, payload);
    };
  };
}

セキュリティ設定

typescript
// src/app.ts
import fastify from 'fastify';

const app = fastify({
  logger: {
    level: process.env.LOG_LEVEL || 'info'
  }
});

// セキュリティプラグイン
await app.register(require('@fastify/helmet'), {
  contentSecurityPolicy: {
    directives: {
      defaultSrc: [`'self'`],
      styleSrc: [`'self'`, `'unsafe-inline'`],
      scriptSrc: [`'self'`],
      imgSrc: [`'self'`, 'data:', 'https:'],
    },
  },
});

await app.register(require('@fastify/cors'), {
  origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
  credentials: true
});

// レート制限
await app.register(require('@fastify/rate-limit'), {
  max: 100,
  timeWindow: '1 minute'
});

// JWT認証設定
await app.register(require('@fastify/jwt'), {
  secret: process.env.JWT_SECRET,
  sign: {
    expiresIn: '1h'
  }
});

運用・保守のベストプラクティス

ヘルスチェックとモニタリング

typescript
// src/controllers/healthController.ts
export async function healthRoutes(fastify: FastifyInstance) {
  fastify.get('/health', async (request, reply) => {
    const checks = await Promise.allSettled([
      // データベース接続確認
      AppDataSource.query('SELECT 1'),
      // Redis接続確認
      redis.ping(),
      // Dify API接続確認
      difyService.healthCheck()
    ]);

    const results = checks.map((check, index) => ({
      name: ['database', 'redis', 'dify'][index],
      status: check.status === 'fulfilled' ? 'healthy' : 'unhealthy',
      ...(check.status === 'rejected' && { error: check.reason.message })
    }));

    const isHealthy = results.every(result => result.status === 'healthy');

    return reply.status(isHealthy ? 200 : 503).send({
      status: isHealthy ? 'healthy' : 'unhealthy',
      timestamp: new Date().toISOString(),
      checks: results
    });
  });

  // メトリクス エンドポイント
  fastify.get('/metrics', async (request, reply) => {
    // Prometheusメトリクス形式で出力
    const metrics = await getApplicationMetrics();
    return reply
      .header('Content-Type', 'text/plain')
      .send(metrics);
  });
}

テスト実装

typescript
// tests/api.test.ts
import { build } from '../src/app';

describe('API Integration Tests', () => {
  let app: any;

  beforeAll(async () => {
    app = build({ logger: false });
    await app.ready();
  });

  afterAll(async () => {
    await app.close();
  });

  test('POST /api/users should create user', async () => {
    const response = await app.inject({
      method: 'POST',
      url: '/api/users',
      payload: {
        email: 'test@example.com',
        name: 'Test User'
      }
    });

    expect(response.statusCode).toBe(201);
    const user = JSON.parse(response.payload);
    expect(user).toHaveProperty('id');
    expect(user.email).toBe('test@example.com');
  });

  test('POST /api/ai/chat should return AI response', async () => {
    // テスト用JWTトークン生成
    const jwt = app.jwt.sign({ id: 1 });

    const response = await app.inject({
      method: 'POST',
      url: '/api/ai/chat',
      headers: {
        authorization: `Bearer ${jwt}`
      },
      payload: {
        message: 'Hello, AI!'
      }
    });

    expect(response.statusCode).toBe(200);
    const result = JSON.parse(response.payload);
    expect(result).toHaveProperty('answer');
    expect(result).toHaveProperty('conversation_id');
  });
});

まとめと次のステップ

この記事で紹介したFastify + TypeORM + Difyの技術スタックにより、以下の効果を実現できます:

  • 開発効率: 従来比50%以上の開発時間短縮
  • パフォーマンス: 76,000+リクエスト/秒の高速API応答
  • コラボレーション: ノーコード連携による非技術者との効果的な協働
  • スケーラビリティ: エンタープライズグレードの拡張性と安定性

今後の展開としては、マイクロサービス化への発展、GraphQL APIの統合、WebSocketを用いたリアルタイム機能の拡張、さらなるAI機能の高度化などが考えられます。

DXプロジェクトの成功には、技術的な優位性だけでなく、開発チームと非技術者との効果的な連携が不可欠です。本記事で紹介した手法が、皆様のプロジェクトの即戦力化に貢献できれば幸いです。

この記事のサンプルコードは、実際のプロジェクトで利用可能な形で記述されています。詳細な実装例や設定ファイルについては、各技術の公式ドキュメントも併せてご参照ください。