Nitroサーバーフレームワーク完全ガイド:ユニバーサルJSサーバー構築


Nitroとは

Nitroは、UnJSエコシステムの一部として開発された次世代のユニバーサルJavaScriptサーバーフレームワークです。Nuxt 3のサーバーエンジンとして誕生しましたが、単体でも使用できる汎用的なフレームワークとして進化しました。

主な特徴

  • ユニバーサルデプロイ: Vercel、Cloudflare、AWS、Netlify など50以上のプラットフォームに対応
  • ファイルベースルーティング: 直感的なAPI設計
  • 自動型生成: TypeScriptの完全サポート
  • ホットリロード: 開発時の高速な反映
  • コード分割: 最適化されたビルド出力
  • ストレージ抽象化: 統一されたデータアクセスAPI

セットアップ

プロジェクト作成

# 新規プロジェクト作成
npx giget@latest nitro my-nitro-app
cd my-nitro-app

# 依存関係のインストール
npm install

# 開発サーバー起動
npm run dev

プロジェクト構造

my-nitro-app/
├── routes/           # APIルート
├── api/             # レガシーAPIルート(非推奨)
├── public/          # 静的ファイル
├── storage/         # ストレージ設定
├── utils/           # ユーティリティ関数
├── plugins/         # サーバープラグイン
├── middleware/      # サーバーミドルウェア
├── nitro.config.ts  # Nitro設定
└── tsconfig.json    # TypeScript設定

ルーティング

基本的なルート定義

// routes/index.ts
export default defineEventHandler(() => {
  return {
    message: 'Hello from Nitro!',
    timestamp: Date.now()
  };
});
// routes/hello.get.ts
export default defineEventHandler((event) => {
  return {
    message: 'GET request to /hello'
  };
});
// routes/hello.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  return {
    message: 'POST request to /hello',
    received: body
  };
});

動的ルート

// routes/users/[id].get.ts
export default defineEventHandler((event) => {
  const id = getRouterParam(event, 'id');
  
  return {
    userId: id,
    name: `User ${id}`,
    email: `user${id}@example.com`
  };
});
// routes/posts/[slug].ts
export default defineEventHandler(async (event) => {
  const slug = getRouterParam(event, 'slug');
  
  // データベースから記事を取得
  const post = await useStorage('posts').getItem(slug);
  
  if (!post) {
    throw createError({
      statusCode: 404,
      message: 'Post not found'
    });
  }
  
  return post;
});

キャッチオールルート

// routes/[...slug].ts
export default defineEventHandler((event) => {
  const slug = getRouterParam(event, 'slug');
  
  return {
    path: slug,
    segments: slug?.split('/') || []
  };
});

リクエスト処理

クエリパラメータの取得

// routes/search.get.ts
export default defineEventHandler((event) => {
  const query = getQuery(event);
  
  return {
    q: query.q,
    page: Number(query.page) || 1,
    limit: Number(query.limit) || 10
  };
});

リクエストボディの処理

// routes/api/users.post.ts
import { z } from 'zod';

const userSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().min(0).optional()
});

export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  
  // バリデーション
  const result = userSchema.safeParse(body);
  
  if (!result.success) {
    throw createError({
      statusCode: 400,
      message: 'Validation failed',
      data: result.error.errors
    });
  }
  
  const user = result.data;
  
  // データベースに保存
  await useStorage('users').setItem(user.email, user);
  
  return {
    success: true,
    user
  };
});

ヘッダーの操作

// routes/api/protected.ts
export default defineEventHandler((event) => {
  const auth = getHeader(event, 'authorization');
  
  if (!auth || !auth.startsWith('Bearer ')) {
    throw createError({
      statusCode: 401,
      message: 'Unauthorized'
    });
  }
  
  const token = auth.substring(7);
  
  // レスポンスヘッダーを設定
  setHeader(event, 'X-Custom-Header', 'value');
  
  return { message: 'Protected resource', token };
});

Cookie操作

// routes/auth/login.post.ts
export default defineEventHandler(async (event) => {
  const { username, password } = await readBody(event);
  
  // 認証処理
  const user = await authenticateUser(username, password);
  
  if (!user) {
    throw createError({
      statusCode: 401,
      message: 'Invalid credentials'
    });
  }
  
  // Cookieを設定
  setCookie(event, 'session', user.sessionToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 60 * 60 * 24 * 7 // 7日間
  });
  
  return { success: true, user };
});

// routes/auth/logout.post.ts
export default defineEventHandler((event) => {
  // Cookieを削除
  deleteCookie(event, 'session');
  
  return { success: true };
});

ミドルウェア

グローバルミドルウェア

// middleware/logger.ts
export default defineEventHandler((event) => {
  const start = Date.now();
  
  console.log(`[${new Date().toISOString()}] ${event.method} ${event.path}`);
  
  // レスポンス後の処理
  event.node.res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`[${new Date().toISOString()}] ${event.method} ${event.path} - ${duration}ms`);
  });
});

認証ミドルウェア

// middleware/auth.ts
export default defineEventHandler(async (event) => {
  // 公開エンドポイントはスキップ
  if (event.path.startsWith('/public') || event.path === '/auth/login') {
    return;
  }
  
  const session = getCookie(event, 'session');
  
  if (!session) {
    throw createError({
      statusCode: 401,
      message: 'Authentication required'
    });
  }
  
  // セッションを検証
  const user = await validateSession(session);
  
  if (!user) {
    throw createError({
      statusCode: 401,
      message: 'Invalid session'
    });
  }
  
  // コンテキストにユーザー情報を保存
  event.context.user = user;
});

CORS設定

// middleware/cors.ts
export default defineEventHandler((event) => {
  setResponseHeaders(event, {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization'
  });
  
  // OPTIONSリクエストの処理
  if (event.method === 'OPTIONS') {
    event.node.res.statusCode = 204;
    event.node.res.end();
    return;
  }
});

ストレージ

ファイルシステムストレージ

// routes/api/cache/[key].get.ts
export default defineEventHandler(async (event) => {
  const key = getRouterParam(event, 'key');
  
  // ストレージから取得
  const value = await useStorage('cache').getItem(key);
  
  if (!value) {
    throw createError({
      statusCode: 404,
      message: 'Key not found'
    });
  }
  
  return { key, value };
});

// routes/api/cache/[key].put.ts
export default defineEventHandler(async (event) => {
  const key = getRouterParam(event, 'key');
  const { value, ttl } = await readBody(event);
  
  // ストレージに保存
  await useStorage('cache').setItem(key, value);
  
  // TTL設定(オプション)
  if (ttl) {
    setTimeout(async () => {
      await useStorage('cache').removeItem(key);
    }, ttl * 1000);
  }
  
  return { success: true, key, value };
});

データベース統合

// utils/db.ts
import { createStorage } from 'unstorage';
import redisDriver from 'unstorage/drivers/redis';

export const redisStorage = createStorage({
  driver: redisDriver({
    host: process.env.REDIS_HOST || 'localhost',
    port: Number(process.env.REDIS_PORT) || 6379,
    password: process.env.REDIS_PASSWORD
  })
});

// routes/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id');
  
  // Redisから取得
  const cached = await redisStorage.getItem(`user:${id}`);
  
  if (cached) {
    return { ...cached, cached: true };
  }
  
  // データベースから取得
  const user = await fetchUserFromDB(id);
  
  // キャッシュに保存
  await redisStorage.setItem(`user:${id}`, user);
  
  return { ...user, cached: false };
});

キャッシング

ルートキャッシュ

// routes/api/expensive.get.ts
export default defineCachedEventHandler(
  async (event) => {
    // 重い処理
    const data = await fetchExpensiveData();
    return data;
  },
  {
    maxAge: 60 * 10, // 10分間キャッシュ
    name: 'expensive-data',
    getKey: (event) => {
      const query = getQuery(event);
      return `expensive-${query.id}`;
    }
  }
);

条件付きキャッシュ

// routes/api/posts.get.ts
export default defineCachedEventHandler(
  async (event) => {
    const posts = await fetchPosts();
    return posts;
  },
  {
    maxAge: 60 * 5,
    shouldBypassCache: (event) => {
      // 管理者はキャッシュをバイパス
      return event.context.user?.role === 'admin';
    }
  }
);

タスクとスケジューリング

// tasks/cleanup.ts
export default defineTask({
  meta: {
    name: 'cleanup',
    description: 'Clean up old cache entries'
  },
  async run() {
    const storage = useStorage('cache');
    const keys = await storage.getKeys();
    
    let cleaned = 0;
    
    for (const key of keys) {
      const item = await storage.getItem(key);
      
      if (isExpired(item)) {
        await storage.removeItem(key);
        cleaned++;
      }
    }
    
    return { result: `Cleaned ${cleaned} items` };
  }
});

// nitro.config.ts で定期実行を設定
export default defineNitroConfig({
  scheduledTasks: {
    // 毎日午前3時に実行
    '0 3 * * *': ['cleanup']
  }
});

WebSocket

// routes/ws.ts
export default defineWebSocketHandler({
  open(peer) {
    console.log('Client connected:', peer.id);
    peer.send({ type: 'welcome', message: 'Connected to server' });
  },
  
  message(peer, message) {
    console.log('Received:', message);
    
    // 全クライアントにブロードキャスト
    peer.publish('chat', message);
  },
  
  close(peer) {
    console.log('Client disconnected:', peer.id);
  },
  
  error(peer, error) {
    console.error('WebSocket error:', error);
  }
});

サーバーサイドレンダリング

// routes/render.get.ts
export default defineEventHandler(async (event) => {
  const html = `
    <!DOCTYPE html>
    <html>
      <head>
        <title>SSR Example</title>
      </head>
      <body>
        <h1>Server-Side Rendered Page</h1>
        <p>Generated at: ${new Date().toISOString()}</p>
      </body>
    </html>
  `;
  
  setResponseHeader(event, 'Content-Type', 'text/html');
  return html;
});

デプロイ

Vercel

// nitro.config.ts
export default defineNitroConfig({
  preset: 'vercel'
});
// vercel.json
{
  "buildCommand": "npm run build",
  "outputDirectory": ".output"
}

Cloudflare Workers

// nitro.config.ts
export default defineNitroConfig({
  preset: 'cloudflare',
  cloudflare: {
    workers: {
      kvNamespaces: ['MY_KV']
    }
  }
});

AWS Lambda

// nitro.config.ts
export default defineNitroConfig({
  preset: 'aws-lambda'
});

Netlify

// nitro.config.ts
export default defineNitroConfig({
  preset: 'netlify'
});

Docker

# Dockerfile
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

ENV PORT=3000
EXPOSE 3000

CMD ["node", ".output/server/index.mjs"]

環境設定

// nitro.config.ts
export default defineNitroConfig({
  runtimeConfig: {
    // プライベート(サーバーサイドのみ)
    dbUrl: process.env.DATABASE_URL,
    apiSecret: process.env.API_SECRET,
    
    // パブリック(クライアントにも露出)
    public: {
      apiBase: process.env.PUBLIC_API_BASE || '/api'
    }
  },
  
  // ルートルール
  routeRules: {
    '/api/**': { cors: true },
    '/public/**': { cache: { maxAge: 60 * 60 } },
    '/admin/**': { ssr: false }
  }
});

使用例:

// routes/api/config.get.ts
export default defineEventHandler((event) => {
  const config = useRuntimeConfig(event);
  
  return {
    // パブリック設定のみ返す
    apiBase: config.public.apiBase,
    // プライベート設定は返さない
  };
});

エラーハンドリング

// middleware/error.ts
export default defineEventHandler((event) => {
  event.node.res.on('error', (error) => {
    console.error('Response error:', error);
  });
});

// utils/errors.ts
export class ApiError extends Error {
  constructor(
    public statusCode: number,
    message: string,
    public code?: string
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

// routes/api/example.ts
export default defineEventHandler(async (event) => {
  try {
    const data = await riskyOperation();
    return data;
  } catch (error) {
    if (error instanceof ApiError) {
      throw createError({
        statusCode: error.statusCode,
        message: error.message,
        data: { code: error.code }
      });
    }
    
    // 予期しないエラー
    throw createError({
      statusCode: 500,
      message: 'Internal server error'
    });
  }
});

テスト

// tests/api.test.ts
import { describe, it, expect } from 'vitest';
import { $fetch, setup } from '@nuxt/test-utils';

describe('API Tests', async () => {
  await setup({
    server: true
  });

  it('GET /api/hello', async () => {
    const response = await $fetch('/api/hello');
    
    expect(response).toHaveProperty('message');
    expect(response.message).toBe('Hello from Nitro!');
  });

  it('POST /api/users', async () => {
    const response = await $fetch('/api/users', {
      method: 'POST',
      body: {
        name: 'John Doe',
        email: 'john@example.com'
      }
    });
    
    expect(response.success).toBe(true);
    expect(response.user.name).toBe('John Doe');
  });
});

ベストプラクティス

1. 型安全性の確保

// types/api.ts
export interface User {
  id: string;
  name: string;
  email: string;
}

export interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
}

// routes/api/users/[id].get.ts
export default defineEventHandler(async (event): Promise<ApiResponse<User>> => {
  const id = getRouterParam(event, 'id');
  const user = await fetchUser(id);
  
  return {
    success: true,
    data: user
  };
});

2. エラーハンドリングの一貫性

// utils/response.ts
export function successResponse<T>(data: T) {
  return {
    success: true,
    data,
    timestamp: Date.now()
  };
}

export function errorResponse(message: string, code?: string) {
  throw createError({
    statusCode: 400,
    message,
    data: { code }
  });
}

3. レート制限

// middleware/rate-limit.ts
const limits = new Map<string, number[]>();

export default defineEventHandler((event) => {
  const ip = getRequestIP(event);
  const now = Date.now();
  const windowMs = 60 * 1000; // 1分
  const maxRequests = 100;
  
  const requests = limits.get(ip) || [];
  const recentRequests = requests.filter(time => now - time < windowMs);
  
  if (recentRequests.length >= maxRequests) {
    throw createError({
      statusCode: 429,
      message: 'Too many requests'
    });
  }
  
  recentRequests.push(now);
  limits.set(ip, recentRequests);
});

まとめ

Nitroは、モダンなサーバーレス環境に最適化された強力なフレームワークです。主な利点:

  • ユニバーサルデプロイメント
  • ファイルベースの直感的なAPI設計
  • TypeScriptの完全サポート
  • 高度なキャッシング機能
  • 豊富なプリセット

適切な設計とベストプラクティスにより、スケーラブルで保守性の高いバックエンドを構築できます。