Code-First Design

Standards Jan 9, 2026 JAVA
code-first api-design design-methodology implementation-first annotations

Definition

Code-first design is an API development approach where you write the implementation code first, using framework-specific annotations or comments to describe API behavior, then automatically generate specifications (like OpenAPI), documentation, and client SDKs from the code. This contrasts with contract-first where you define the specification before implementing.

The core idea is that your source code is the single source of truth. Annotations like @Path("/users"), @ApiResponse, or decorator metadata describe endpoints, schemas, and validation rules directly in the code. Build-time or runtime tools scan the codebase, extract these annotations, and generate OpenAPI specs, Swagger UI documentation, and type definitions.

Code-first appeals to developers who prefer working in their programming language rather than YAML/JSON. It promises “write code once, get docs free.” Many popular frameworks (Spring Boot, ASP.NET Core, FastAPI, NestJS) have excellent code-first tooling with annotations that generate specs automatically. However, it trades design-phase benefits for implementation convenience.

Example

Spring Boot REST API: A Java developer annotates controller classes with @RestController, @GetMapping, @PostMapping, and uses @ApiOperation from Springdoc. When the app runs, Springdoc generates an OpenAPI spec at /v3/api-docs and serves Swagger UI at /swagger-ui.html - all automatically derived from code annotations.

FastAPI in Python: Python developers use type hints and Pydantic models to define endpoints. FastAPI introspects these types at runtime and auto-generates OpenAPI specs, interactive docs, and JSON Schema validation - no manual spec writing required.

ASP.NET Core: C# developers decorate controllers with attributes like [HttpGet], [ProducesResponseType], and XML comments. Swashbuckle parses these at build time, generating OpenAPI specs that power API documentation and client SDK generation.

NestJS TypeScript: Developers use decorators like @Get(), @Body(), @ApiProperty() on DTOs. NestJS’s Swagger module reflects on these decorators to generate OpenAPI specs matching the actual implementation.

Rapid Prototyping: A startup quickly builds an MVP by writing Flask or Express endpoints. Once routes work, they add minimal annotations and generate OpenAPI specs for frontend teams, avoiding upfront spec design when requirements are unclear.

Analogy

Write First, Outline Later: Code-first is like writing an essay by immediately drafting paragraphs, then using a tool to extract headings and generate a table of contents from the text. Contract-first is like outlining the essay structure first, getting feedback on the outline, then writing paragraphs to fill in each section.

Recorded Music: Code-first is like a band improvising a song in the studio, recording it, then someone transcribes it to sheet music. Contract-first is like writing the sheet music first, reviewing it with the band, then recording exactly what was written.

Construction Without Blueprints: Code-first is like skilled builders constructing a shed based on experience and intuition, then having an architect create as-built drawings documenting what was built. Contract-first is the traditional approach - architect draws blueprints first, builders follow them precisely.

Code Example

Spring Boot (Java) - Code-First with Annotations


import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.responses.*;
import javax.validation.Valid;
import javax.validation.constraints.*;

@RestController
@RequestMapping("/api/users")
@Tag(name = "Users", description = "User management endpoints")
public class UserController {

    @GetMapping("/{id}")
    @Operation(summary = "Get user by ID", description = "Returns a single user")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "User found",
            content = @Content(schema = @Schema(implementation = User.class))),
        @ApiResponse(responseCode = "404", description = "User not found",
            content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
    })
    public User getUserById(@PathVariable String id) {
        return userService.findById(id);
    }

    @PostMapping
    @Operation(summary = "Create a new user")
    @ApiResponse(responseCode = "201", description = "User created",
        content = @Content(schema = @Schema(implementation = User.class)))
    @ApiResponse(responseCode = "400", description = "Invalid input")
    public User createUser(@Valid @RequestBody CreateUserRequest request) {
        return userService.create(request);
    }
}

@Schema(description = "User entity")
class User {
    @Schema(description = "Unique user identifier", example = "123e4567-e89b-12d3-a456-426614174000")
    private String id;

    @Schema(description = "User's email address", example = "[email protected]")
    @Email
    private String email;

    @Schema(description = "User's display name", example = "Alice Smith")
    @NotBlank
    @Size(min = 3, max = 50)
    private String name;

    // getters, setters
}

class CreateUserRequest {
    @Email
    @NotBlank
    private String email;

    @NotBlank
    @Size(min = 3, max = 50)
    private String name;

    @Size(min = 12, max = 128)
    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*?&]{12,}$")
    private String password;

    // getters, setters
}

class ErrorResponse {
    private String code;
    private String message;
    // getters, setters
}

Generated OpenAPI spec (automatic):


# This is auto-generated by Springdoc at /v3/api-docs
openapi: 3.0.1
info:
  title: User API
  version: 1.0.0
paths:
  /api/users/{id}:
    get:
      tags:
        - Users
      summary: Get user by ID
      description: Returns a single user
      operationId: getUserById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: User found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/users:
    post:
      tags:
        - Users
      summary: Create a new user
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
      responses:
        '201':
          description: User created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          description: Invalid input

components:
  schemas:
    User:
      type: object
      description: User entity
      properties:
        id:
          type: string
          description: Unique user identifier
          example: 123e4567-e89b-12d3-a456-426614174000
        email:
          type: string
          format: email
          description: User's email address
          example: [email protected]
        name:
          type: string
          minLength: 3
          maxLength: 50
          description: User's display name
          example: Alice Smith

    CreateUserRequest:
      type: object
      required: [email, name, password]
      properties:
        email:
          type: string
          format: email
        name:
          type: string
          minLength: 3
          maxLength: 50
        password:
          type: string
          minLength: 12
          maxLength: 128
          pattern: "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*?&]{12,}$"

    ErrorResponse:
      type: object
      properties:
        code:
          type: string
        message:
          type: string

FastAPI (Python) - Code-First with Type Hints


from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime

app = FastAPI(title="User API", version="1.0.0")

# Pydantic models define schemas automatically
class CreateUserRequest(BaseModel):
    email: EmailStr
    name: str = Field(..., min_length=3, max_length=50)
    password: str = Field(..., min_length=12, max_length=128)

class User(BaseModel):
    id: str = Field(..., description="Unique user identifier")
    email: EmailStr = Field(..., description="User's email address")
    name: str = Field(..., description="User's display name")
    created_at: datetime

    class Config:
        schema_extra = {
            "example": {
                "id": "123e4567-e89b-12d3-a456-426614174000",
                "email": "[email protected]",
                "name": "Alice Smith",
                "created_at": "2024-01-01T10:00:00Z"
            }
        }

@app.get("/api/users/{user_id}",
         response_model=User,
         summary="Get user by ID",
         responses={404: {"description": "User not found"}})
async def get_user(user_id: str):
    """Returns a single user by ID"""
    user = await user_service.find_by_id(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user

@app.post("/api/users",
          response_model=User,
          status_code=201,
          summary="Create a new user")
async def create_user(request: CreateUserRequest):
    """Creates a new user account"""
    return await user_service.create(request)

# FastAPI auto-generates OpenAPI at /openapi.json
# Serves interactive docs at /docs (Swagger UI)
# Serves alternative docs at /redoc (ReDoc)

Diagram

graph TB
    subgraph "Code-First Flow"
        START[Write Implementation
With Annotations] START --> BUILD[Build/Runtime
Process] BUILD --> EXTRACT[Extract Annotations
Reflection/AST] EXTRACT --> GEN_SPEC[Generate OpenAPI Spec
Automatic] GEN_SPEC --> DOCS[Swagger UI
Documentation] GEN_SPEC --> SDK[Generate SDKs
From Spec] GEN_SPEC --> VALIDATE[Validation
From Spec] subgraph "Challenges" DRIFT[Spec Drift Risk
If annotations incomplete] DESIGN[No Design Phase
Stakeholder Review] REFACTOR[Refactoring Impact
Breaks API] end GEN_SPEC -.Risks.-> DRIFT START -.Risks.-> DESIGN START -.Risks.-> REFACTOR end style START fill:#87CEEB style GEN_SPEC fill:#90EE90 style DRIFT fill:#FFB6C6 style DESIGN fill:#FFB6C6

Best Practices

  1. Annotate thoroughly - Don’t rely on defaults, explicitly annotate responses, errors, examples
  2. Version from the start - Even code-first benefits from explicit versioning (e.g., /v1/users)
  3. Validate generated spec - Review auto-generated OpenAPI for completeness and accuracy
  4. Export and version specs - Commit generated specs to git for tracking API evolution
  5. Use framework validation - Let framework annotations handle input validation (e.g., @Valid)
  6. Document examples in code - Use annotation features to provide realistic examples
  7. Combine with contract testing - Generate spec, then test implementation against it
  8. Consider hybrid approach - Start code-first for prototyping, switch to contract-first for production

Common Mistakes

Incomplete annotations: Relying on framework defaults without explicit annotations for error responses, authentication, or examples. Generated specs have gaps.

Ignoring generated spec: Auto-generating the spec but never reviewing it. Clients might get poor documentation or incorrect schemas.

No spec versioning: Generating specs on-the-fly without committing them to version control. No history of API changes, hard to detect breaking changes.

Refactoring breaks API: Renaming a class field breaks the API response schema without warning. Code-first lacks early warning systems.

Framework lock-in: Heavy use of framework-specific annotations makes migration difficult. Spec is not framework-agnostic.

Missing design review: Skipping stakeholder review of API design because “the code is the spec.” Design flaws discovered late.

Annotation overload: Code becomes cluttered with so many documentation annotations it’s hard to read the business logic.

Standards & RFCs

Standards & RFCs
1)- [OpenAPI 3](https://reference.apios.info/terms/openapi-3/).1.0 Specification
2)- JSON Schema Draft 2020-12
3)- JSR 380 - Bean Validation 2.0 (Java)
4)- Swagger Annotations (Java)
5)- Pydantic (Python type validation)