Welcome back! In previous chapters, we established the specification as the single source of truth and explored how it defines our system’s behavior and structure. But what’s the next logical step after defining a perfect spec? It’s turning that spec into working code, automatically and consistently.

This chapter dives into the exciting world of code generation and transformation in Spec-Driven Development (SDD). We’ll learn how to leverage our carefully crafted specifications to automatically produce boilerplate code, API clients, data models, and even entire infrastructure configurations. This not only speeds up development but also drastically reduces human error and ensures that your code always aligns with your latest specifications. We’ll also touch upon the growing role of AI in making these transformations even smarter and more efficient.

Ready to see your specs come to life as code? Let’s get started!

Core Concepts: Bridging the Gap from Spec to Code

The journey from a high-level specification to functional code can often be tedious, involving repetitive tasks like writing data models, API interfaces, and validation logic. Code generation is the bridge that automates much of this work.

The “Why” of Code Generation

Why bother generating code when you can write it by hand? The reasons are compelling, especially when thinking about system consistency and developer velocity:

  • Reduces Boilerplate: Many parts of software development are repetitive, like defining CRUD operations for a resource or creating API client stubs. Code generation handles this grunt work, freeing developers to focus on unique business logic.
  • Ensures Consistency: When code is generated from a single source (the spec), it inherently adheres to the rules and structures defined in that spec. This eliminates inconsistencies that often arise from manual coding by different developers or teams working on parts of a distributed system.
  • Eliminates Human Error: Manual coding is prone to typos, forgotten fields, or misinterpretations of the spec. Generated code is deterministic; if the spec is correct, the generated code will be correct according to the generator’s rules, reducing debugging time for common mistakes.
  • Accelerates Development: New features or changes to an API can instantly propagate to client SDKs or backend implementations through regeneration, drastically cutting down development cycles from days to minutes.
  • Keeps Code in Sync: As your specifications evolve, regenerating the code ensures that your codebase always reflects the latest agreed-upon design, preventing drift between documentation and implementation.

Types of Code Generation and Transformation

Code generation isn’t a one-size-fits-all solution. It manifests in various forms, each serving a specific purpose in the development lifecycle:

  • Scaffolding: This is often the first step, generating the basic project structure, initial files, and configuration needed to start a new project or module. Think of tools like create-react-app or dotnet new.
  • Boilerplate Generation: This involves generating repetitive code based on a detailed spec, such as:
    • API Clients/SDKs: From an OpenAPI (formerly Swagger) specification, generating client libraries in various languages (TypeScript, Java, Python).
    • Data Models/Schemas: Generating classes or structs that represent the data structures defined in your spec.
    • Database Migrations: Generating schema changes from a data model definition, ensuring the database schema aligns with your application’s data models.
  • Transformation: This is a more general concept where a specification in one format is transformed into code or configuration in another.
    • Business Requirements to Infrastructure as Code (IaC): Translating high-level needs (e.g., “a scalable web service”) into specific cloud resource definitions (e.g., Terraform, AWS CloudFormation).
    • Domain-Specific Language (DSL) to General-Purpose Code: Converting a custom, declarative language into executable code, often used in specialized domains.

The Role of AI in Spec-to-Code

The integration of AI and agentic development is rapidly transforming code generation, making it more powerful and intuitive. Instead of rigid templates, AI can act as a “smart transformer” that understands context and intent.

  • Natural Language to Code: AI can interpret natural language descriptions of requirements or specifications and translate them into structured code, even suggesting implementations for complex logic beyond simple data structures.
  • Filling in Gaps and Suggesting Implementations: Where a spec might be high-level or incomplete, AI can propose detailed code snippets, database schemas, or test cases, acting as an intelligent assistant that proactively helps complete the design.
  • Agentic Development: This emerging paradigm, exemplified by projects like Specky and IBM’s IAC Spec Kit, uses specialized AI agents to automate entire development workflows. An agent might:
    • Read a user story (spec).
    • Generate API endpoints.
    • Write database migrations.
    • Even create pull requests with tests and documentation.
    • These agents often maintain “durable task state” and “agent context,” meaning they remember previous steps and decisions, allowing for more complex, multi-phase automation that goes beyond a single-shot generation.

๐Ÿ“Œ Key Idea: AI-assisted code generation moves beyond simple template filling to intelligent interpretation and synthesis, making SDD even more potent by automating decision-making and implementation details.

Let’s visualize the basic flow of spec-to-code generation, including the powerful influence of AI:

flowchart TD Spec[Specification] --> Generator[Code Generator] Generator --> Generated_Code[Generated Code] Generated_Code --> Dev_Team[Development Team] Spec --> AI_Agent[AI Assistant Agent] AI_Agent -->|Refines Spec| Spec AI_Agent -->|Suggests Code| Generator
  • Specification: The single source of truth (e.g., OpenAPI, business requirements, DSL).
  • Code Generator: A tool that takes the spec and applies rules/templates to produce code.
  • Generated Code: The output, ready to be integrated into your project.
  • Development Team: Integrates, builds upon, and maintains the generated code.
  • AI Assistant Agent: An intelligent system that can interpret, refine, or even directly generate code from specs. It can influence the Specification itself (e.g., suggesting improvements) or provide direct code suggestions to the Code Generator.

Step-by-Step Implementation: Generating an API Client from an OpenAPI Spec

One of the most common and impactful applications of code generation is creating API clients from an OpenAPI specification. This ensures that any client interacting with your API uses the correct endpoints, data structures, and request/response formats, providing strong type safety and reducing integration errors.

For this example, we’ll use the openapi-generator-cli tool, a powerful command-line interface for the OpenAPI Generator project.

Prerequisites:

Before we begin, ensure you have:

  • Node.js and npm: We’ll use npm to install our generator. As of 2026-07-25, Node.js v20.x LTS or v22.x Current is recommended. You can download it from nodejs.org.
  • A basic understanding of YAML.

Step 1: Define a Sample OpenAPI Spec (YAML)

First, let’s create a simple OpenAPI specification. This spec will describe a basic “Todo” API with operations to list and create todos.

Create a new project directory (e.g., sdd-codegen-example) and inside it, create a file named openapi.yaml.

# openapi.yaml
openapi: 3.0.0
info:
  title: Todo API
  version: 1.0.0
  description: A simple API for managing todo items.

servers:
  - url: http://localhost:3000/api
    description: Local development server

paths:
  /todos:
    get:
      summary: List all todos
      operationId: listTodos
      responses:
        '200':
          description: A list of todo items.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Todo'
    post:
      summary: Create a new todo
      operationId: createTodo
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewTodo'
      responses:
        '201':
          description: The created todo item.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Todo'

components:
  schemas:
    Todo:
      type: object
      required:
        - id
        - title
        - completed
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the todo item.
        title:
          type: string
          description: The title of the todo item.
        completed:
          type: boolean
          description: Whether the todo item is completed.
          default: false
    NewTodo:
      type: object
      required:
        - title
      properties:
        title:
          type: string
          description: The title of the new todo item.

Explanation of the openapi.yaml file:

  • openapi: 3.0.0: This line explicitly declares that we are adhering to version 3.0.0 of the OpenAPI Specification.
  • info: This section provides general metadata about your API, including its title, version, and a description.
  • servers: Here, you define the base URL(s) where your API can be accessed. For this example, we’re using a local development server.
  • paths: This is the core of your API definition, where you specify all available endpoints.
    • /todos: This defines the endpoint for interacting with todo items.
      • get: Describes the HTTP GET operation for this path, used to retrieve a list of all todos.
        • summary: A short, human-readable description of the operation.
        • operationId: A unique string used to identify the operation. This is crucial as it will often be used to generate the method name in client SDKs.
        • responses: Defines the possible responses for this operation. A 200 response indicates success, and its content specifies that it returns an array of Todo objects in JSON format.
      • post: Describes the HTTP POST operation, used to create a new todo.
        • requestBody: Defines the data expected in the request body. required: true means it must be provided. The schema references NewTodo.
        • responses: A 201 response indicates successful creation, returning the newly created Todo object.
  • components/schemas: This section is used to define reusable data structures (schemas) that can be referenced throughout your API.
    • Todo: This schema defines the structure of a complete todo item, including an id, title, and completed status. required lists the properties that must always be present.
    • NewTodo: This schema defines the structure for creating a new todo. Notice it only requires title, as id and completed status are typically set by the server upon creation.

Step 2: Install openapi-generator-cli

Open your terminal in the sdd-codegen-example directory and initialize a new Node.js project. Then, install openapi-generator-cli as a development dependency.

# Initialize a new npm project
npm init -y

# Install openapi-generator-cli
npm install @openapitools/[email protected] --save-dev

โšก Quick Note: As of 2026-07-25, @openapitools/openapi-generator-cli version 2.13.4 is the latest stable release. Always check the official npm page for the absolute latest if you’re working on a real project, or the GitHub repository for the OpenAPI Generator project: https://github.com/OpenAPITools/openapi-generator.

Step 3: Generate the Client

Now, let’s use the installed CLI tool to generate a TypeScript client from our openapi.yaml file. We’ll specify the typescript-fetch generator, which is a common choice for web clients due to its reliance on the standard fetch API.

# Generate the TypeScript client
npx @openapitools/openapi-generator-cli generate \
  -i openapi.yaml \
  -g typescript-fetch \
  -o ./generated-client

Explanation of the generate command:

  • npx @openapitools/openapi-generator-cli generate: This command invokes the generate subcommand of the openapi-generator-cli tool. npx is a Node.js package runner that allows you to execute local node_modules executables without explicitly adding them to your PATH.
  • -i openapi.yaml: This flag specifies the input file, which is our OpenAPI specification in YAML format.
  • -g typescript-fetch: This flag instructs the generator to use the typescript-fetch template. The OpenAPI Generator supports many different language and framework generators (e.g., java, python, go, spring, angular), each producing idiomatic code for its target.
  • -o ./generated-client: This flag specifies the output directory where all the generated code will be placed. If the directory doesn’t exist, the tool will create it.

After running this command, you should see a new directory named generated-client created in your project.

Step 4: Explore the Generated Code

Navigate into the generated-client directory. You’ll find a structured output with various files, which together form a complete, type-safe API client. Let’s look at some key ones:

generated-client/
โ”œโ”€โ”€ api.ts
โ”œโ”€โ”€ configuration.ts
โ”œโ”€โ”€ index.ts
โ”œโ”€โ”€ runtime.ts
โ””โ”€โ”€ tsconfig.json
  • api.ts: This file contains the actual API client methods and the TypeScript interfaces for your data models. Open it and look for DefaultApi. You’ll find methods like listTodos and createTodo, directly corresponding to the operationIds in your openapi.yaml.

    // Snippet from generated-client/api.ts (simplified for clarity)
    // This file contains the core API client logic, including type definitions
    // and methods for interacting with your API.
    
    export interface Todo {
        'id': string;
        'title': string;
        'completed': boolean;
    }
    
    export interface NewTodo {
        'title': string;
    }
    
    // ... (serialization/deserialization helper functions) ...
    
    export class DefaultApi extends BaseAPI {
        /**
         * List all todos
         */
        async listTodos(initOverrides?: RequestOpts): Promise<Array<Todo>> {
            // ... internal implementation details using fetch API ...
            const response = await this.request(context);
            // Deserialization logic: parses JSON response into Todo objects
            return new runtime.JSONApiResponse(response, (json) => (json.map(TodoFromJSON)));
        }
    
        /**
         * Create a new todo
         */
        async createTodo(requestParameters: CreateTodoRequest, initOverrides?: RequestOpts): Promise<Todo> {
            // ... internal implementation details for POST request ...
            const response = await this.request(context);
            // Deserialization logic: parses JSON response into a single Todo object
            return new runtime.JSONApiResponse(response, (json) => TodoFromJSON(json));
        }
    }

    Notice how the methods (listTodos, createTodo) are strongly typed, expecting specific request parameters (like CreateTodoRequest for createTodo) and returning specific response types (Array<Todo>, Todo). This is the core power of spec-driven generation: compile-time safety and self-documenting code.

  • configuration.ts: This file typically provides a Configuration class where you can set global API client settings like the base URL, authentication tokens, and custom fetch options. This allows you to configure the generated client for different environments (development, staging, production) without modifying the generated code itself.

What to Observe/Learn:

  • The generated code is strongly typed, providing compile-time safety and better developer experience by catching potential errors before runtime.
  • The API methods are directly derived from your spec’s paths and operationIds, maintaining a direct link between design and implementation.
  • The data models (interfaces) are exact representations of your spec’s schemas, ensuring data consistency across your application.
  • You didn’t write a single line of client-side boilerplate for API calls or data modeling, yet you have a fully functional and type-safe API client! This significantly reduces development effort and potential for human error.

Mini-Challenge: Extend the OpenAPI Spec and Regenerate

Now it’s your turn to make a change and see the power of regeneration in action. This exercise will reinforce how quickly changes in your specification propagate to your codebase.

Challenge: Add a new endpoint to your openapi.yaml for retrieving a single todo item by its ID. Call the operation getTodoById. Also, add a new field dueDate (of type string with format: date) to the Todo schema.

Steps:

  1. Open your openapi.yaml file in your sdd-codegen-example directory.
  2. Add a new path /todos/{todoId} with a get operation. Remember that {todoId} signifies a path parameter.
  3. Define the todoId as a path parameter, specifying its name, in (path), required, and schema (e.g., type: string, format: uuid).
  4. Specify a 200 response for this new get operation that returns a single Todo object.
  5. Add the dueDate property to the Todo schema within the components/schemas section. Make it type: string and format: date.
  6. Save the openapi.yaml file.
  7. Run the openapi-generator-cli command again from your terminal, pointing to the same input and output:
    npx @openapitools/openapi-generator-cli generate \
      -i openapi.yaml \
      -g typescript-fetch \
      -o ./generated-client
    You might be prompted to overwrite existing files. For this exercise, confirming Y (yes) is fine. In a real-world CI/CD pipeline, you’d typically ensure the output directory is clean before regeneration.
  8. Inspect the generated-client/api.ts file to find your new getTodoById method and the updated Todo interface, which should now include dueDate.

Hint: For the path parameter, your get operation for /todos/{todoId} will need a parameters section. For dueDate, simply add it under properties in the Todo schema, similar to title or completed.

What to Observe/Learn:

  • How quickly a change in the specification translates to an updated, type-safe client, often in seconds.
  • The new getTodoById method appearing in api.ts, complete with type definitions for its todoId parameter.
  • The dueDate property being added to the Todo interface in api.ts, ensuring all consumers of this model are aware of the new field.
  • This hands-on experience reinforces the “single source of truth” principle and the efficiency gains inherent in SDD. Any change in the spec immediately reflects in the generated code, maintaining consistency across the system.

Common Pitfalls & Troubleshooting

While code generation is powerful, it’s not without its challenges. Understanding these common pitfalls can save you a lot of headaches and ensure a smoother SDD workflow.

  • Outdated Generators or Templates:
    • Problem: Using an older version of a code generator that doesn’t support the latest OpenAPI spec features (e.g., new security schemes, callback objects), or a generator whose default templates produce suboptimal or outdated code (e.g., using deprecated language features).
    • Solution: Always check the generator’s documentation for supported spec versions and generator options. Keep your openapi-generator-cli (or similar tools) updated to their latest stable releases. Regularly review the generated code for modern best practices and consider contributing to the generator’s templates if you find common improvements.
  • Over-customization of Generated Code:
    • Problem: Developers often feel the urge to hand-edit the generated files to add custom logic, fix minor issues, or tweak formatting. This is a trap! The moment you hand-edit, your code is no longer truly “generated,” and your changes will be lost the next time you regenerate, leading to frustrating merge conflicts or lost work.
    • Solution: Treat generated code as immutable. If you need to add custom logic, do so in separate files that use the generated client/models, but explicitly do not modify the generated files themselves. Most generators offer extension points (e.g., custom interceptors, partial templates) or ways to provide custom templates if absolutely necessary.
  • Complex Specs Leading to Unwieldy Code:
    • Problem: A poorly structured, overly complex, or inconsistent OpenAPI spec can lead to generated code that is difficult to understand, use, or debug. For example, deeply nested schemas, inconsistent naming conventions, or ambiguous descriptions.
    • Solution: Invest time in crafting a clear, concise, and consistent specification. Remember the adage “garbage in, garbage out.” The quality of your spec directly impacts the quality of your generated code. Use tools like Spectral to lint and validate your OpenAPI specs against style guides and best practices.
  • Configuration Mismatch:
    • Problem: The generated code might have default configurations (e.g., base URL, authentication headers, API keys) that don’t match your specific environment (development, staging, production). This can lead to runtime errors or security vulnerabilities.
    • Solution: Most generators provide options to customize runtime configuration. For openapi-generator-cli with typescript-fetch, you’ll find a Configuration class in configuration.ts where you can pass base paths, credentials, and other runtime settings when initializing the API client. Always externalize configuration for different environments.

Summary

In this chapter, we’ve taken a significant leap from just defining specifications to actively transforming them into usable code. This is where the true power of Spec-Driven Development begins to shine, creating a direct, automated link between design and implementation.

Here are the key takeaways from our exploration:

  • Code generation is crucial for SDD as it automates boilerplate, ensures consistency, eliminates human error, and significantly accelerates development cycles.
  • It encompasses various forms, including scaffolding, boilerplate generation, and broader transformations from one spec format to another (e.g., business requirements to IaC).
  • AI and agentic development are revolutionizing code generation by enabling intelligent interpretation of natural language specs and automating complex multi-phase workflows, moving beyond simple template filling.
  • We demonstrated a practical example by generating a type-safe TypeScript API client from an OpenAPI specification using openapi-generator-cli, highlighting the immediate benefits of strong typing and automation.
  • Treat generated code as immutable and focus on creating clean, consistent specifications to get the best results, avoiding the temptation to manually edit generated files.

You’ve now seen how your specifications can directly drive your codebase, making development faster, more reliable, and more consistent. But how do we ensure that only code generated from valid, approved specs makes it into our main branches? That’s where gated workflows and enforceable specs come into play, which we’ll explore in the next chapter!

References

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.