森 宝松 SIer Tech Blog

【現場で求められるAPI設計の勘所~SIerエンジニアのための実践ガイド~】

森 宝松
SIer Tech Blog
2025年3月19日

【現場で求められるAPI設計の勘所~SIerエンジニアのための実践ガイド~】

SIerの現場では、システム間連携やマイクロサービス化の流れを受けて、APIの設計・開発の機会が増えています。本記事では、SIerエンジニアが押さえておくべきAPI設計のポイントを、実践的な観点から解説します。

1. API設計の基本原則

1.1 API設計で押さえるべき3つの視点

API設計において最も重要なのは、以下の3つの視点のバランスを取ることです:

  1. ビジネス要件との整合性

    • APIはビジネスプロセスを反映すべき
    • 業務フローに沿った操作を提供する
    • 将来の拡張性を考慮する
  2. 使いやすさ(Developer Experience)

    • 直感的で理解しやすいインターフェース
    • 一貫性のある命名規則
    • 適切なドキュメント
  3. 保守性・運用性

    • バージョン管理のしやすさ
    • モニタリングと障害対応
    • パフォーマンスチューニング

1.2 RESTful APIの設計原則

RESTful APIを設計する際の基本原則:

// URIの設計例
GET    /api/v1/customers           # 顧客一覧の取得
GET    /api/v1/customers/{id}      # 特定顧客の取得
POST   /api/v1/customers           # 新規顧客の作成
PUT    /api/v1/customers/{id}      # 顧客情報の更新
DELETE /api/v1/customers/{id}      # 顧客の削除

// 関連リソースの表現
GET    /api/v1/customers/{id}/orders    # 特定顧客の注文一覧
POST   /api/v1/customers/{id}/orders    # 特定顧客の注文作成

1.2.1 リソース指向の設計

  • リソースの明確な識別: URIでリソースを一意に特定
  • HTTPメソッドの適切な使用: GET, POST, PUT, DELETEの使い分け
  • ステートレス: セッション状態に依存しない

1.2.2 レスポンス設計

// 成功レスポンスの例
{
  "status": "success",
  "data": {
    "customerId": "C001",
    "name": "山田太郎",
    "email": "yamada@example.com",
    "createdAt": "2025-03-18T10:00:00Z"
  }
}

// エラーレスポンスの例
{
  "status": "error",
  "code": "INVALID_PARAMETER",
  "message": "無効なパラメータが指定されました",
  "details": [
    {
      "field": "email",
      "message": "メールアドレスの形式が不正です"
    }
  ]
}

2. 要件定義とAPI設計プロセス

2.1 要件の整理と分析

2.1.1 ビジネス要件の把握

  • 業務フローの理解
    • 現行システムの分析
    • ステークホルダーへのヒアリング
    • 将来的な拡張性の検討

2.1.2 技術要件の整理

  • 性能要件

    • レスポンスタイム
    • スループット
    • 同時接続数
  • セキュリティ要件

    • 認証・認可方式
    • データ保護
    • 監査ログ

2.2 API設計ドキュメントの作成

2.2.1 API仕様書の例(OpenAPI/Swagger)

openapi: 3.0.0
info:
  title: 顧客管理API
  version: 1.0.0
paths:
  /customers:
    get:
      summary: 顧客一覧の取得
      parameters:
        - name: page
          in: query
          schema:
            type: integer
        - name: limit
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Customer'
    post:
      summary: 新規顧客の作成
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerCreate'
      responses:
        '201':
          description: 作成成功
components:
  schemas:
    Customer:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string

3. セキュリティ設計

3.1 認証・認可の実装

3.1.1 JWT(JSON Web Token)による認証

// JWTトークンの生成
@Service
public class JwtTokenService {
    private final String secretKey;
    
    public String generateToken(User user) {
        return Jwts.builder()
            .setSubject(user.getUsername())
            .claim("roles", user.getRoles())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 24時間
            .signWith(SignatureAlgorithm.HS256, secretKey)
            .compact();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return true;
        } catch (JwtException e) {
            return false;
        }
    }
}

3.1.2 OAuth 2.0による認可

// OAuth 2.0の設定例(Spring Security)
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/api/v1/public/**").permitAll()
            .antMatchers("/api/v1/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated();
    }
}

3.2 セキュリティヘッダーの設定

// セキュリティヘッダーの設定例
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .headers()
                .xssProtection()
                .and()
                .contentSecurityPolicy("default-src 'self'")
                .and()
                .frameOptions().deny()
                .and()
                .httpStrictTransportSecurity()
                    .includeSubDomains(true)
                    .maxAgeInSeconds(31536000);
    }
}

4. エラーハンドリングとバリデーション

4.1 エラーハンドリング

4.1.1 共通エラーハンドラー

@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(ValidationException ex) {
        ErrorResponse error = new ErrorResponse(
            "VALIDATION_ERROR",
            "入力値が不正です",
            ex.getValidationErrors()
        );
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            "NOT_FOUND",
            "リソースが見つかりません",
            Collections.singletonList(ex.getMessage())
        );
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
        ErrorResponse error = new ErrorResponse(
            "INTERNAL_ERROR",
            "内部エラーが発生しました",
            Collections.singletonList(ex.getMessage())
        );
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

4.2 入力バリデーション

4.2.1 リクエストバリデーション

// リクエストDTOの例
public class CustomerCreateRequest {
    @NotBlank(message = "名前は必須です")
    @Size(max = 100, message = "名前は100文字以内で入力してください")
    private String name;
    
    @NotBlank(message = "メールアドレスは必須です")
    @Email(message = "メールアドレスの形式が不正です")
    private String email;
    
    @Pattern(regexp = "^[0-9]{10,11}$", message = "電話番号の形式が不正です")
    private String phone;
    
    // getters and setters
}

// コントローラーでのバリデーション
@RestController
@RequestMapping("/api/v1/customers")
public class CustomerController {
    
    @PostMapping
    public ResponseEntity<CustomerResponse> createCustomer(
            @Valid @RequestBody CustomerCreateRequest request) {
        // バリデーション済みのリクエストを処理
        Customer customer = customerService.createCustomer(request);
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(CustomerResponse.from(customer));
    }
}

5. パフォーマンスとスケーラビリティ

5.1 キャッシュ戦略

5.1.1 HTTPキャッシュ

// キャッシュヘッダーの設定例
@GetMapping("/api/v1/products/{id}")
public ResponseEntity<ProductResponse> getProduct(@PathVariable Long id) {
    Product product = productService.findById(id);
    return ResponseEntity
        .ok()
        .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
        .eTag(product.getVersion().toString())
        .body(ProductResponse.from(product));
}

5.1.2 アプリケーションキャッシュ

// Spring Cacheの使用例
@Service
public class ProductService {
    
    @Cacheable(value = "products", key = "#id")
    public Product findById(Long id) {
        // データベースからの取得処理
        return productRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Product not found"));
    }
    
    @CacheEvict(value = "products", key = "#id")
    public void updateProduct(Long id, ProductUpdateRequest request) {
        // 更新処理
    }
}

5.2 ページネーションと部分レスポンス

// ページネーションの実装例
@GetMapping("/api/v1/products")
public ResponseEntity<PageResponse<ProductResponse>> getProducts(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size,
        @RequestParam(required = false) String fields) {
    
    Pageable pageable = PageRequest.of(page, size);
    Page<Product> products = productRepository.findAll(pageable);
    
    // フィールドフィルタリング
    List<ProductResponse> responses = products.getContent().stream()
        .map(product -> ProductResponse.from(product, fields))
        .collect(Collectors.toList());
    
    PageResponse<ProductResponse> response = new PageResponse<>(
        responses,
        products.getNumber(),
        products.getSize(),
        products.getTotalElements()
    );
    
    return ResponseEntity.ok(response);
}

6. API運用と監視

6.1 ログ設計

6.1.1 アクセスログ

// インターセプターによるアクセスログの実装
@Component
public class ApiLoggingInterceptor implements HandlerInterceptor {
    
    private static final Logger logger = LoggerFactory.getLogger(ApiLoggingInterceptor.class);
    
    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler) {
        
        String requestId = UUID.randomUUID().toString();
        request.setAttribute("requestId", requestId);
        
        logger.info("Request: [{}] {} {} (Client IP: {})",
            requestId,
            request.getMethod(),
            request.getRequestURI(),
            request.getRemoteAddr()
        );
        
        return true;
    }
    
    @Override
    public void afterCompletion(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler,
            Exception ex) {
        
        String requestId = (String) request.getAttribute("requestId");
        
        logger.info("Response: [{}] Status: {} (Processing Time: {}ms)",
            requestId,
            response.getStatus(),
            System.currentTimeMillis() - request.getAttribute("startTime")
        );
    }
}

6.2 メトリクス収集

// Prometheusメトリクスの設定例
@Configuration
public class MetricsConfig {
    
    @Bean
    public MeterRegistry meterRegistry() {
        return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
    }
    
    @Bean
    public TimedAspect timedAspect(MeterRegistry registry) {
        return new TimedAspect(registry);
    }
}

@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
    
    private final Counter orderCounter;
    private final Timer orderProcessingTimer;
    
    public OrderController(MeterRegistry registry) {
        this.orderCounter = registry.counter("api.orders.created");
        this.orderProcessingTimer = registry.timer("api.orders.processing_time");
    }
    
    @PostMapping
    @Timed(value = "api.orders.create", description = "注文作成の処理時間")
    public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
        orderCounter.increment();
        
        return orderProcessingTimer.record(() -> {
            Order order = orderService.createOrder(request);
            return ResponseEntity.status(HttpStatus.CREATED)
                .body(OrderResponse.from(order));
        });
    }
}

7. バージョニング戦略

7.1 URLベースのバージョニング

// URLパスでのバージョン管理
/api/v1/customers    # Version 1
/api/v2/customers    # Version 2

7.2 ヘッダーベースのバージョニング

@RestController
@RequestMapping("/api/customers")
public class CustomerController {
    
    @GetMapping(headers = "API-Version=1")
    public ResponseEntity<CustomerResponseV1> getCustomerV1(@PathVariable Long id) {
        // Version 1の実装
    }
    
    @GetMapping(headers = "API-Version=2")
    public ResponseEntity<CustomerResponseV2> getCustomerV2(@PathVariable Long id) {
        // Version 2の実装
    }
}

8. ドキュメント作成

8.1 Swaggerによる API ドキュメント

@Configuration
@EnableSwagger2
public class SwaggerConfig {
    
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
            .select()
            .apis(RequestHandlerSelectors.basePackage("com.example.api"))
            .paths(PathSelectors.ant("/api/**"))
            .build()
            .apiInfo(apiInfo());
    }
    
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
            .title("顧客管理システム API")
            .description("顧客情報の管理を行うためのRESTful API")
            .version("1.0.0")
            .build();
    }
}

@RestController
@RequestMapping("/api/v1/customers")
@Api(tags = "顧客管理API")
public class CustomerController {
    
    @ApiOperation(value = "顧客情報の取得", notes = "指定されたIDの顧客情報を取得します")
    @ApiResponses({
        @ApiResponse(code = 200, message = "成功"),
        @ApiResponse(code = 404, message = "顧客が見つかりません")
    })
    @GetMapping("/{id}")
    public ResponseEntity<CustomerResponse> getCustomer(
            @ApiParam(value = "顧客ID", required = true)
            @PathVariable Long id) {
        // 実装
    }
}

9. まとめ

API設計は、技術的な側面だけでなく、ビジネス要件やユーザビリティ、運用性など、多面的な考慮が必要な作業です。SIerの現場では特に以下の点に注意を払うことが重要です:

  1. 要件の明確化

    • ビジネス要件の深い理解
    • 将来的な拡張性の考慮
    • ステークホルダーとの合意形成
  2. 設計品質の確保

    • 一貫性のある設計方針
    • 適切なセキュリティ対策
    • 効果的なエラーハンドリング
  3. 運用性の考慮

    • 監視とログ設計
    • パフォーマンス最適化
    • バージョン管理戦略
  4. ドキュメント化

    • API仕様書の整備
    • 開発者向けガイドの作成
    • サンプルコードの提供

これらの要素を適切にバランスを取りながら設計することで、長期的に維持可能で価値のあるAPIを提供することができます。

参考文献

  1. 「RESTful Web APIs」 - Leonard Richardson, Mike Amundsen
  2. 「Web API: The Good Parts」 - 水野貴明
  3. 「Clean Architecture」 - Robert C. Martin
  4. 「マイクロサービスパターン」 - Chris Richardson

API設計は継続的な学習と改善が必要な分野です。本記事で紹介した内容を基礎として、実際のプロジェクトでの経験を重ねながら、より良いAPI設計のスキルを磨いていってください。

関連記事

2025/3/25

【「動作保証はどこまで?」SIerのためのシステム保守の基本】

SIerエンジニアのためのシステム保守ガイド。業務システムの保守範囲の定義から具体的な保守活動まで、実践的なアプローチを解説します。

2025/3/24

【SIerが知るべきログ設計のベストプラクティス】

SIerエンジニアのためのログ設計ガイド。業務システムにおける効果的なログ設計から運用管理まで、実践的なベストプラクティスを解説します。

2025/3/23

【長年運用されている業務システムの"負債"とどう向き合うか?】

SIerエンジニアのための技術的負債管理ガイド。長年運用されてきた業務システムの負債を理解し、効果的に管理・改善していくための実践的なアプローチを解説します。