スキル一覧に戻る
lastcow

innozverse-api-style

by lastcow

Production-grade monorepo web application with authentication, built with Next.js, Hono, Flutter, and TypeScript

0🍴 0📅 2026年1月5日
GitHubで見るManusで実行

SKILL.md


name: innozverse-api-style description: Follow API development conventions including RESTful design, Fastify patterns, Zod validation, error handling, and versioning. Use when building API endpoints, adding routes, or working with API code.

innozverse API Development Style

When developing API endpoints for innozverse, follow these patterns and conventions.

Fastify Basics

Route Registration

// apps/api/src/routes/v1/users.ts
import { FastifyInstance } from 'fastify';

export async function usersRoutes(fastify: FastifyInstance) {
  fastify.get('/users', async (request, reply) => {
    return { users: [] };
  });

  fastify.get('/users/:id', async (request, reply) => {
    const { id } = request.params as { id: string };
    return { user: { id } };
  });

  fastify.post('/users', async (request, reply) => {
    const body = request.body;
    return reply.code(201).send({ user: body });
  });
}

Register in Index

// apps/api/src/index.ts
import { usersRoutes } from './routes/v1/users';

fastify.register(usersRoutes, { prefix: '/v1' });

Response Patterns

Success Response

return reply.code(200).send({
  status: 'ok',
  data: { /* ... */ }
});

Created Response

return reply.code(201).send({
  status: 'created',
  data: { id: newId }
});

Error Response

return reply.code(400).send({
  error: 'ValidationError',
  message: 'Invalid input',
  statusCode: 400
});

Type Safety

Define Types in @innozverse/shared

// packages/shared/src/types.ts
export interface User {
  id: string;
  name: string;
  email: string;
}

export interface UserResponse {
  status: 'ok';
  data: User;
}

Use in API

import { User, UserResponse } from '@innozverse/shared';

fastify.get<{ Reply: UserResponse }>('/users/:id', async (request, reply) => {
  const user: User = { /* ... */ };
  return reply.send({
    status: 'ok',
    data: user
  });
});

Validation with Zod

Define Schema in @innozverse/shared

// packages/shared/src/schemas.ts
import { z } from 'zod';

export const userSchema = z.object({
  name: z.string().min(1),
  email: z.string().email()
});

Use in API

import { userSchema } from '@innozverse/shared';

fastify.post('/users', async (request, reply) => {
  try {
    const validated = userSchema.parse(request.body);
    // Use validated data
    return reply.code(201).send({ data: validated });
  } catch (error) {
    return reply.code(400).send({
      error: 'ValidationError',
      message: error.message
    });
  }
});

Error Handling

Global Error Handler

// apps/api/src/index.ts
fastify.setErrorHandler((error, request, reply) => {
  fastify.log.error(error);

  reply.status(error.statusCode || 500).send({
    error: error.name || 'InternalServerError',
    message: error.message || 'Something went wrong',
    statusCode: error.statusCode || 500
  });
});

Throwing Errors

fastify.get('/users/:id', async (request, reply) => {
  const user = await findUser(id);

  if (!user) {
    return reply.code(404).send({
      error: 'NotFound',
      message: 'User not found',
      statusCode: 404
    });
  }

  return { data: user };
});

Async/Await

Always use async/await, never callbacks:

// ✅ Good
fastify.get('/users', async (request, reply) => {
  const users = await getUsers();
  return { users };
});

// ❌ Bad
fastify.get('/users', (request, reply) => {
  getUsers((err, users) => {
    reply.send({ users });
  });
});

RESTful Conventions

Resource Naming

  • Plural nouns: /users, /posts
  • Nested resources: /users/:userId/posts

HTTP Methods

  • GET /resource - List all
  • GET /resource/:id - Get one
  • POST /resource - Create
  • PUT /resource/:id - Replace
  • PATCH /resource/:id - Update
  • DELETE /resource/:id - Delete

Status Codes

  • 200 - Success (GET, PUT, PATCH, DELETE)
  • 201 - Created (POST)
  • 204 - No Content (DELETE)
  • 400 - Bad Request (validation error)
  • 401 - Unauthorized
  • 403 - Forbidden
  • 404 - Not Found
  • 409 - Conflict (duplicate)
  • 500 - Internal Server Error

Versioning

Always version API routes:

// ✅ Good
fastify.register(v1Routes, { prefix: '/v1' });
fastify.register(v2Routes, { prefix: '/v2' });

// ❌ Bad
fastify.register(routes); // No version

CORS Configuration

import cors from '@fastify/cors';

await fastify.register(cors, {
  origin: process.env.CORS_ORIGIN || '*',
  credentials: true
});

Environment Variables

const PORT = parseInt(process.env.PORT || '8080', 10);
const NODE_ENV = process.env.NODE_ENV || 'development';

// Never hardcode secrets
const DB_URL = process.env.DATABASE_URL; // ✅
const API_KEY = process.env.API_KEY; // ✅

Logging

// Use Fastify's built-in logger
fastify.log.info('Server starting');
fastify.log.error({ err: error }, 'Error occurred');
fastify.log.debug({ data }, 'Debug info');

Health Check

Always maintain a health check endpoint:

fastify.get('/health', async () => ({
  status: 'ok',
  timestamp: new Date().toISOString(),
  version: process.env.API_VERSION || '1.0.0'
}));

Testing (Future)

// apps/api/src/routes/__tests__/users.test.ts
import { buildServer } from '../../index';

describe('Users API', () => {
  let fastify;

  beforeAll(async () => {
    fastify = await buildServer();
  });

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

  test('GET /v1/users returns users list', async () => {
    const response = await fastify.inject({
      method: 'GET',
      url: '/v1/users'
    });

    expect(response.statusCode).toBe(200);
    expect(response.json()).toHaveProperty('users');
  });
});

Database Patterns (Future)

When adding database:

// Use a connection pool
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL
});

// Close connections gracefully
fastify.addHook('onClose', async () => {
  await pool.end();
});

// Use in routes
fastify.get('/users/:id', async (request, reply) => {
  const { rows } = await pool.query(
    'SELECT * FROM users WHERE id = $1',
    [request.params.id]
  );

  if (rows.length === 0) {
    return reply.code(404).send({ error: 'Not found' });
  }

  return { data: rows[0] };
});

Best Practices

Single Responsibility

Each route file handles one resource:

routes/v1/
├── users.ts      # User management
├── posts.ts      # Post management
└── comments.ts   # Comment management

DRY Principles

Extract common logic:

// utils/auth.ts
export async function requireAuth(request, reply) {
  const token = request.headers.authorization;
  if (!token) {
    return reply.code(401).send({ error: 'Unauthorized' });
  }
  // Verify token
}

// routes/v1/users.ts
fastify.get('/users/me', {
  preHandler: requireAuth
}, async (request, reply) => {
  return { user: request.user };
});

Graceful Shutdown

process.on('SIGTERM', async () => {
  await fastify.close();
  process.exit(0);
});

Anti-Patterns to Avoid

❌ Don't use any types:

// Bad
fastify.get('/users', async (request: any, reply: any) => {

❌ Don't block the event loop:

// Bad
fastify.get('/heavy', async () => {
  let result = 0;
  for (let i = 0; i < 1000000000; i++) {
    result += i;
  }
  return { result };
});

❌ Don't ignore errors:

// Bad
fastify.get('/users', async () => {
  const users = await getUsers().catch(() => []);
  return { users };
});

❌ Don't expose internal errors to clients:

// Bad
return reply.code(500).send({ error: error.stack });

// Good
fastify.log.error(error);
return reply.code(500).send({ error: 'Internal Server Error' });

References

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です