Full Stack • Java • System Design • Cloud • AI Engineering

Build a Code Generator with Spring AI

A detailed step-by-step guide to build a safe code generator using Spring Boot, Spring AI ChatClient, structured output, validation, and a real Spring REST API generation example.

Build a Code Generator with Spring AI

A code generator converts a developer's natural language requirement into source code.

Example input:

Create a Spring Boot CRUD API for products with id, name, price, and stock.

Expected output:

  • Product.java
  • ProductRequest.java
  • ProductResponse.java
  • ProductService.java
  • ProductController.java
  • Unit test skeleton

In this guide, we will build a Spring AI code generator that creates a small Spring Boot REST API module from a feature request.

The focus is not only generation. We will also add safety:

  • Use structured output instead of one giant text blob.
  • Validate generated file paths.
  • Allow only selected file extensions.
  • Block path traversal like ../../.
  • Return files as JSON first.
  • Optionally write generated files to a safe output directory.

What We Are Building

We will expose these APIs:

API Method Purpose
/api/codegen/health GET Check service health
/api/codegen/generate POST Generate code and return it as JSON
/api/codegen/write POST Generate code and write files into a safe folder

Real Use Case

A developer asks:

Generate a Spring Boot REST API for managing tasks.
Fields: id, title, description, status, dueDate.
Use in-memory storage. Include controller, service, DTOs, and model.

The API returns:

{
  "projectName": "task-api",
  "summary": "Spring Boot task management REST API using in-memory storage.",
  "files": [
    {
      "path": "src/main/java/com/example/task/model/Task.java",
      "language": "java",
      "content": "package com.example.task.model; ..."
    }
  ]
}

Architecture

flowchart TD
    User["Developer Requirement"] --> Controller["CodeGeneratorController"]
    Controller --> Service["CodeGeneratorService"]
    Service --> Prompt["Prompt + Coding Rules"]
    Prompt --> ChatClient["Spring AI ChatClient"]
    ChatClient --> Model["AI Model"]
    Model --> Structured["GeneratedProject"]
    Structured --> Validator["CodeOutputValidator"]
    Validator -->|Valid| Response["JSON Response"]
    Validator -->|Invalid| Error["Reject Output"]
    Response --> OptionalWrite["Optional Safe File Writer"]

Safety Rules

Rule Why
Return structured files Easier to validate than raw markdown
Validate file paths Prevent writing outside output folder
Allow known extensions only Avoid scripts or binary payloads
Block absolute paths Prevent overwriting system files
Block .. path traversal Prevent escaping safe directory
Write only under configured folder Keeps generated code contained
Review generated code before production use AI code can contain bugs

Tools and Frameworks

Tool Recommended Version Purpose
Java 21 or later Runtime
Spring Boot 4.0.x REST API
Spring AI 2.0.0 ChatClient and structured output
OpenAI Current API Code generation model
Maven 3.9+ Build tool
curl or Postman Any current version Testing

Project Structure

spring-ai-code-generator/
├── pom.xml
└── src/
    └── main/
        ├── java/
        │   └── com/
        │       └── codewithvenu/
        │           └── codegen/
        │               ├── CodeGeneratorApplication.java
        │               ├── controller/
        │               │   └── CodeGeneratorController.java
        │               ├── dto/
        │               │   ├── CodeFile.java
        │               │   ├── CodeGenerationRequest.java
        │               │   ├── GeneratedProject.java
        │               │   └── WriteProjectResponse.java
        │               ├── exception/
        │               │   └── GlobalExceptionHandler.java
        │               └── service/
        │                   ├── CodeGeneratorService.java
        │                   ├── CodeOutputValidator.java
        │                   └── SafeProjectWriter.java
        └── resources/
            └── application.yml

Step 1: Create Maven Project

File: pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.0</version>
        <relativePath/>
    </parent>

    <groupId>com.codewithvenu</groupId>
    <artifactId>spring-ai-code-generator</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-ai-code-generator</name>

    <properties>
        <java.version>21</java.version>
        <spring-ai.version>2.0.0</spring-ai.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-openai</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Step 2: Configure Spring AI

File: src/main/resources/application.yml

server:
  port: 8080

spring:
  application:
    name: spring-ai-code-generator
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4.1-mini
          temperature: 0.1

codegen:
  output-dir: generated-code

Set your OpenAI API key:

export OPENAI_API_KEY="your-openai-api-key-here"

Windows PowerShell:

$env:OPENAI_API_KEY="your-openai-api-key-here"

Step 3: Main Application Class

File: CodeGeneratorApplication.java

package com.codewithvenu.codegen;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CodeGeneratorApplication {

    public static void main(String[] args) {
        SpringApplication.run(CodeGeneratorApplication.class, args);
    }
}

Step 4: Create DTOs

CodeGenerationRequest

File: dto/CodeGenerationRequest.java

package com.codewithvenu.codegen.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record CodeGenerationRequest(
    @NotBlank(message = "requirement is required")
    @Size(max = 4000, message = "requirement must be less than 4000 characters")
    String requirement,

    String basePackage,

    String projectName,

    boolean includeTests
) {
    public String safeBasePackage() {
        if (basePackage == null || basePackage.isBlank()) {
            return "com.example.generated";
        }
        return basePackage;
    }

    public String safeProjectName() {
        if (projectName == null || projectName.isBlank()) {
            return "generated-project";
        }
        return projectName;
    }
}

CodeFile

File: dto/CodeFile.java

package com.codewithvenu.codegen.dto;

public record CodeFile(
    String path,
    String language,
    String content,
    String purpose
) {
}

GeneratedProject

File: dto/GeneratedProject.java

package com.codewithvenu.codegen.dto;

import java.util.List;

public record GeneratedProject(
    String projectName,
    String summary,
    List<String> assumptions,
    List<CodeFile> files,
    List<String> runInstructions
) {
}

WriteProjectResponse

File: dto/WriteProjectResponse.java

package com.codewithvenu.codegen.dto;

import java.time.Instant;
import java.util.List;

public record WriteProjectResponse(
    String projectName,
    String outputDirectory,
    List<String> writtenFiles,
    Instant createdAt
) {
}

Step 5: Create Output Validator

File: service/CodeOutputValidator.java

package com.codewithvenu.codegen.service;

import com.codewithvenu.codegen.dto.CodeFile;
import com.codewithvenu.codegen.dto.GeneratedProject;
import org.springframework.stereotype.Service;

import java.nio.file.Path;
import java.util.List;
import java.util.Set;

@Service
public class CodeOutputValidator {

    private static final Set<String> ALLOWED_EXTENSIONS = Set.of(
        ".java",
        ".xml",
        ".yml",
        ".yaml",
        ".md",
        ".properties",
        ".json"
    );

    public void validate(GeneratedProject project) {
        if (project == null) {
            throw new IllegalArgumentException("Generated project is empty");
        }

        if (project.files() == null || project.files().isEmpty()) {
            throw new IllegalArgumentException("Generated project does not contain files");
        }

        for (CodeFile file : project.files()) {
            validateFile(file);
        }
    }

    private void validateFile(CodeFile file) {
        if (file.path() == null || file.path().isBlank()) {
            throw new IllegalArgumentException("Generated file path is empty");
        }

        String path = file.path().replace("\\", "/");

        if (path.startsWith("/") || path.contains("../") || path.contains("..\\")) {
            throw new IllegalArgumentException("Unsafe generated file path: " + file.path());
        }

        Path normalized = Path.of(path).normalize();
        if (normalized.startsWith("..")) {
            throw new IllegalArgumentException("Generated file escapes output directory: " + file.path());
        }

        boolean extensionAllowed = ALLOWED_EXTENSIONS.stream().anyMatch(path::endsWith);
        if (!extensionAllowed) {
            throw new IllegalArgumentException("Generated file extension is not allowed: " + file.path());
        }

        if (file.content() == null || file.content().isBlank()) {
            throw new IllegalArgumentException("Generated file content is empty: " + file.path());
        }
    }

    public List<String> safePaths(GeneratedProject project) {
        return project.files()
            .stream()
            .map(CodeFile::path)
            .toList();
    }
}

Step 6: Create Safe File Writer

File: service/SafeProjectWriter.java

package com.codewithvenu.codegen.service;

import com.codewithvenu.codegen.dto.CodeFile;
import com.codewithvenu.codegen.dto.GeneratedProject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.List;

@Service
public class SafeProjectWriter {

    private final Path outputRoot;

    public SafeProjectWriter(@Value("${codegen.output-dir:generated-code}") String outputDir) {
        this.outputRoot = Path.of(outputDir).toAbsolutePath().normalize();
    }

    public List<String> write(GeneratedProject project) {
        Path projectDir = outputRoot.resolve(safeDirectoryName(project.projectName())).normalize();

        if (!projectDir.startsWith(outputRoot)) {
            throw new IllegalArgumentException("Unsafe project output directory");
        }

        try {
            Files.createDirectories(projectDir);

            for (CodeFile file : project.files()) {
                Path target = projectDir.resolve(file.path()).normalize();

                if (!target.startsWith(projectDir)) {
                    throw new IllegalArgumentException("Generated file escapes project directory: " + file.path());
                }

                Files.createDirectories(target.getParent());
                Files.writeString(target, file.content(), StandardCharsets.UTF_8);
            }
        }
        catch (IOException ex) {
            throw new IllegalStateException("Failed to write generated project", ex);
        }

        return project.files()
            .stream()
            .map(file -> projectDir.resolve(file.path()).normalize().toString())
            .toList();
    }

    public String outputRoot() {
        return outputRoot.toString();
    }

    private String safeDirectoryName(String projectName) {
        String name = projectName == null || projectName.isBlank()
            ? "project-" + Instant.now().toEpochMilli()
            : projectName;

        return name.replaceAll("[^A-Za-z0-9._-]", "-");
    }
}

Step 7: Create Code Generator Service

File: service/CodeGeneratorService.java

package com.codewithvenu.codegen.service;

import com.codewithvenu.codegen.dto.CodeGenerationRequest;
import com.codewithvenu.codegen.dto.GeneratedProject;
import com.codewithvenu.codegen.dto.WriteProjectResponse;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.List;

@Service
public class CodeGeneratorService {

    private final ChatClient chatClient;
    private final CodeOutputValidator validator;
    private final SafeProjectWriter writer;

    public CodeGeneratorService(
        ChatClient.Builder builder,
        CodeOutputValidator validator,
        SafeProjectWriter writer
    ) {
        this.chatClient = builder
            .defaultSystem("""
                You are a senior Java and Spring Boot code generator.

                Generate clean, simple, copy-ready code.
                Return structured output only.

                Rules:
                - Generate files for a small Spring Boot module.
                - Use Java 21.
                - Use package names from the provided basePackage.
                - Use constructor injection.
                - Use records for request/response DTOs when appropriate.
                - Do not include markdown fences inside file content.
                - Do not generate absolute file paths.
                - Do not generate files outside the project folder.
                - Keep generated code beginner-friendly.
                """)
            .build();
        this.validator = validator;
        this.writer = writer;
    }

    public GeneratedProject generate(CodeGenerationRequest request) {
        GeneratedProject project = chatClient
            .prompt()
            .user(user -> user
                .text("""
                    Generate a Spring Boot code module.

                    Requirement:
                    {requirement}

                    Project name:
                    {projectName}

                    Base package:
                    {basePackage}

                    Include tests:
                    {includeTests}

                    Required files:
                    - model classes
                    - request and response DTOs
                    - service class
                    - REST controller
                    - README.md with run and test instructions
                    - tests if includeTests is true

                    Return a GeneratedProject with file paths relative to project root.
                    """)
                .param("requirement", request.requirement())
                .param("projectName", request.safeProjectName())
                .param("basePackage", request.safeBasePackage())
                .param("includeTests", request.includeTests()))
            .call()
            .entity(GeneratedProject.class, spec -> spec.validateSchema());

        validator.validate(project);
        return project;
    }

    public WriteProjectResponse generateAndWrite(CodeGenerationRequest request) {
        GeneratedProject project = generate(request);
        List<String> writtenFiles = writer.write(project);

        return new WriteProjectResponse(
            project.projectName(),
            writer.outputRoot(),
            writtenFiles,
            Instant.now()
        );
    }
}

Step 8: Create Controller

File: controller/CodeGeneratorController.java

package com.codewithvenu.codegen.controller;

import com.codewithvenu.codegen.dto.CodeGenerationRequest;
import com.codewithvenu.codegen.dto.GeneratedProject;
import com.codewithvenu.codegen.dto.WriteProjectResponse;
import com.codewithvenu.codegen.service.CodeGeneratorService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/api/codegen")
public class CodeGeneratorController {

    private final CodeGeneratorService codeGeneratorService;

    public CodeGeneratorController(CodeGeneratorService codeGeneratorService) {
        this.codeGeneratorService = codeGeneratorService;
    }

    @GetMapping("/health")
    public Map<String, String> health() {
        return Map.of("status", "UP", "service", "spring-ai-code-generator");
    }

    @PostMapping("/generate")
    public GeneratedProject generate(@Valid @RequestBody CodeGenerationRequest request) {
        return codeGeneratorService.generate(request);
    }

    @PostMapping("/write")
    public WriteProjectResponse write(@Valid @RequestBody CodeGenerationRequest request) {
        return codeGeneratorService.generateAndWrite(request);
    }
}

Step 9: Add Error Handling

File: exception/GlobalExceptionHandler.java

package com.codewithvenu.codegen.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex) {
        Map<String, String> fields = new HashMap<>();

        ex.getBindingResult().getFieldErrors().forEach(error ->
            fields.put(error.getField(), error.getDefaultMessage())
        );

        Map<String, Object> body = new HashMap<>();
        body.put("timestamp", Instant.now());
        body.put("status", HttpStatus.BAD_REQUEST.value());
        body.put("error", "Validation failed");
        body.put("fields", fields);

        return ResponseEntity.badRequest().body(body);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<Map<String, Object>> handleBadRequest(IllegalArgumentException ex) {
        Map<String, Object> body = new HashMap<>();
        body.put("timestamp", Instant.now());
        body.put("status", HttpStatus.BAD_REQUEST.value());
        body.put("error", "Bad request");
        body.put("message", ex.getMessage());

        return ResponseEntity.badRequest().body(body);
    }
}

Step 10: Run the Application

mvn spring-boot:run

Health check:

curl http://localhost:8080/api/codegen/health

Expected output:

{
  "service": "spring-ai-code-generator",
  "status": "UP"
}

Step 11: Generate Code as JSON

Input:

curl -X POST http://localhost:8080/api/codegen/generate \
  -H "Content-Type: application/json" \
  -d '{
    "projectName": "task-api",
    "basePackage": "com.example.task",
    "includeTests": true,
    "requirement": "Create a Spring Boot REST API for managing tasks. Fields: id, title, description, status, dueDate. Use in-memory storage. Include create, list, find by id, update status, and delete endpoints."
  }'

Expected output shape:

{
  "projectName": "task-api",
  "summary": "Spring Boot REST API for managing tasks with in-memory storage.",
  "assumptions": [
    "Task status is represented as a string or enum.",
    "Persistence is in-memory for demo purposes."
  ],
  "files": [
    {
      "path": "src/main/java/com/example/task/model/Task.java",
      "language": "java",
      "purpose": "Domain model",
      "content": "package com.example.task.model;\n\n..."
    }
  ],
  "runInstructions": [
    "Run mvn spring-boot:run",
    "Use POST /api/tasks to create a task"
  ]
}

Step 12: Write Generated Files

Input:

curl -X POST http://localhost:8080/api/codegen/write \
  -H "Content-Type: application/json" \
  -d '{
    "projectName": "task-api",
    "basePackage": "com.example.task",
    "includeTests": true,
    "requirement": "Create a Spring Boot REST API for managing tasks. Fields: id, title, description, status, dueDate. Use in-memory storage. Include create, list, find by id, update status, and delete endpoints."
  }'

Expected output:

{
  "projectName": "task-api",
  "outputDirectory": "/path/to/spring-ai-code-generator/generated-code",
  "writtenFiles": [
    "/path/to/spring-ai-code-generator/generated-code/task-api/src/main/java/com/example/task/model/Task.java",
    "/path/to/spring-ai-code-generator/generated-code/task-api/src/main/java/com/example/task/controller/TaskController.java"
  ],
  "createdAt": "2026-06-23T10:15:30Z"
}

The generated project is written under:

generated-code/task-api/

Step 13: Example Generated API Design

For the task API requirement, generated endpoints should look like:

Method Endpoint Purpose
POST /api/tasks Create task
GET /api/tasks List tasks
GET /api/tasks/{id} Find task by ID
PATCH /api/tasks/{id}/status Update task status
DELETE /api/tasks/{id} Delete task

Example generated request DTO:

package com.example.task.dto;

import jakarta.validation.constraints.NotBlank;
import java.time.LocalDate;

public record CreateTaskRequest(
    @NotBlank String title,
    String description,
    LocalDate dueDate
) {
}

Example generated controller style:

package com.example.task.controller;

import com.example.task.dto.CreateTaskRequest;
import com.example.task.dto.TaskResponse;
import com.example.task.service.TaskService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/tasks")
public class TaskController {

    private final TaskService taskService;

    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }

    @PostMapping
    public TaskResponse create(@Valid @RequestBody CreateTaskRequest request) {
        return taskService.create(request);
    }

    @GetMapping
    public List<TaskResponse> findAll() {
        return taskService.findAll();
    }
}

Step 14: Code Generation Flow

sequenceDiagram
    participant Dev as Developer
    participant API as CodeGeneratorController
    participant Service as CodeGeneratorService
    participant AI as ChatClient / Model
    participant Validator as CodeOutputValidator
    participant Writer as SafeProjectWriter

    Dev->>API: POST /api/codegen/write
    API->>Service: generateAndWrite(request)
    Service->>AI: requirement + coding rules
    AI-->>Service: GeneratedProject
    Service->>Validator: validate(project)
    Validator-->>Service: valid
    Service->>Writer: write(project)
    Writer-->>Service: written file paths
    Service-->>API: WriteProjectResponse
    API-->>Dev: JSON response

Prompt Design Tips

Good code generation prompts include:

  • Framework version.
  • Java version.
  • Package name.
  • Required files.
  • Coding conventions.
  • Feature requirement.
  • Whether tests are needed.
  • What not to generate.

Bad prompt:

Build task API.

Better prompt:

Create a Spring Boot REST API for managing tasks.
Use Java 21 and constructor injection.
Fields: id, title, description, status, dueDate.
Use in-memory storage.
Include controller, service, model, DTOs, validation, and tests.

Production Improvements

Before production, add:

  1. Run generated code in a sandbox.
  2. Format generated files with google-java-format or Spotless.
  3. Compile generated projects automatically.
  4. Run tests automatically.
  5. Scan generated dependencies for vulnerabilities.
  6. Add policy checks for forbidden APIs.
  7. Require human review before merging code.
  8. Store generation history and prompts.
  9. Add per-user rate limits.
  10. Support project templates.

Common Mistakes

Mistake Problem Better Approach
Asking for raw markdown output Hard to validate Use structured output
Writing generated files anywhere Security risk Restrict output directory
No path validation Path traversal risk Block absolute paths and ..
No compile step Broken code may pass API Compile generated project
No human review Bugs or insecure code Review before production
Huge requirements Lower quality output Generate one feature at a time

Complete Test Script

curl http://localhost:8080/api/codegen/health

curl -X POST http://localhost:8080/api/codegen/generate \
  -H "Content-Type: application/json" \
  -d '{"projectName":"task-api","basePackage":"com.example.task","includeTests":true,"requirement":"Create a Spring Boot REST API for managing tasks. Fields: id, title, description, status, dueDate. Use in-memory storage. Include create, list, find by id, update status, and delete endpoints."}'

curl -X POST http://localhost:8080/api/codegen/write \
  -H "Content-Type: application/json" \
  -d '{"projectName":"product-api","basePackage":"com.example.product","includeTests":false,"requirement":"Create a Spring Boot REST API for products. Fields: id, name, description, price, stock. Use in-memory storage. Include create, list, find by id, update stock, and delete endpoints."}'

Summary

You built a Spring AI code generator that:

  1. Accepts a natural language feature request.
  2. Uses ChatClient to generate code.
  3. Returns structured output as GeneratedProject.
  4. Validates generated file paths and extensions.
  5. Optionally writes files into a safe folder.
  6. Produces copy-ready Spring Boot REST API code.

The key lesson:

Code generation should be treated like a controlled build pipeline, not a free-form text response.

Use structured output, validation, sandboxing, compile checks, and human review before production use.

References