Spring Boot Skills
GitHub
API Design

OpenAPI First

Use when the project follows API-first / OpenAPI-first approach: generating controller interfaces, DTOs, and clients from an OpenAPI spec. Use when you see openapi.yaml, openapi-generator-maven-plugin, or ApiDelegate pattern in the project.

View SKILL.md

Maven Plugin Setup

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>7.5.0</version>
    <executions>
        <execution>
            <goals><goal>generate</goal></goals>
            <configuration>
                <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
                <generatorName>spring</generatorName>
                <apiPackage>com.example.api</apiPackage>
                <modelPackage>com.example.api.model</modelPackage>
                <configOptions>
                    <delegatePattern>true</delegatePattern>      <!-- implement delegate, not controller -->
                    <interfaceOnly>false</interfaceOnly>
                    <useSpringBoot3>true</useSpringBoot3>
                    <useTags>true</useTags>
                    <dateLibrary>java8</dateLibrary>
                    <openApiNullable>false</openApiNullable>
                    <skipDefaultInterface>false</skipDefaultInterface> <!-- must stay false with delegatePattern: the controller instantiates a no-op delegate -->
                </configOptions>
                <!-- NOT generateSupportingFiles=true: that adds a second @SpringBootApplication main class
                     (repackage fails: "Unable to find a single main class") and a SpringDocConfiguration -->
                <supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate>
                <modelNameSuffix>Dto</modelNameSuffix> <!-- OrderDto, not Order: no clash with domain types -->
                <output>${project.build.directory}/generated-sources/openapi</output>
            </configuration>
        </execution>
    </executions>
</plugin>

OpenAPI Spec Example

# src/main/resources/openapi.yaml
openapi: 3.0.3
info:
  title: Order Service API
  version: 1.0.0

paths:
  /api/v1/orders:
    post:
      tags: [Orders]
      operationId: createOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
      responses:
        '201':
          description: Order created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderResponse'
        '400':
          $ref: '#/components/responses/ValidationError'

    get:
      tags: [Orders]
      operationId: listOrders
      parameters:
        - name: page
          in: query
          schema: { type: integer, default: 0 }
        - name: size
          in: query
          schema: { type: integer, default: 20 }
      responses:
        '200':
          description: Paginated orders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderPage'

components:
  schemas:
    CreateOrderRequest:
      type: object
      required: [customerEmail, items]
      properties:
        customerEmail:
          type: string
          format: email
        items:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/OrderItemRequest'

    OrderResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED]
        customerEmail:
          type: string
        createdAt:
          type: string
          format: date-time

    OrderItemRequest:
      type: object
      required: [productId, quantity]
      properties:
        productId:
          type: string
          format: uuid
        quantity:
          type: integer
          minimum: 1

    OrderPage:
      type: object
      properties:
        content:
          type: array
          items:
            $ref: '#/components/schemas/OrderResponse'
        page:
          type: integer
        size:
          type: integer
        totalElements:
          type: integer
          format: int64

    ApiError:
      type: object
      properties:
        code:
          type: string
        message:
          type: string
        details:
          type: array
          items:
            type: string

  responses:
    ValidationError:
      description: Validation failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'

Implementing the Delegate

// Generated: OrdersApi interface with delegate
// Your implementation — never modify generated files

@Service
@RequiredArgsConstructor
public class OrdersApiDelegateImpl implements OrdersApiDelegate {

    private final OrderService orderService;

    @Override
    public ResponseEntity<OrderResponse> createOrder(CreateOrderRequest request) {
        Order order = orderService.createOrder(request);
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(OrderApiMapper.toResponse(order));
    }

    @Override
    public ResponseEntity<OrderPage> listOrders(Integer page, Integer size) {
        Page<Order> orders = orderService.findAll(PageRequest.of(page, size));
        return ResponseEntity.ok(OrderApiMapper.toPage(orders));
    }
}

.gitignore — Never Commit Generated Files

target/generated-sources/openapi/

Gotchas

  • Agent modifies generated controller files — NEVER modify generated code, implement delegate
  • Agent generates code without useSpringBoot3=true — uses old javax.* imports
  • Agent commits generated sources — add to .gitignore, generate on build
  • Agent sets skipDefaultInterface=true with delegatePattern=true — the generated controller instantiates a no-op delegate, so it stops compiling; keep it false and cover every operation with a contract test
  • Agent sets generateSupportingFiles=true — generates a second @SpringBootApplication and SpringDoc config; list only ApiUtil.java in supportingFilesToGenerate
  • Agent mixes generated models with hand-written models — keep them separate
FROM THE REPOSITORY

Examples & templates

bad-delegate-impl.javaexamples
Source on GitHub
// ❌ BAD — modifies generated controller, mixes models, wrong config

// Editing the GENERATED controller directly — will be overwritten on next build!
@RestController
@RequestMapping("/api/v1/orders")
public class OrdersApiController {         // this file is generated — don't touch it

    @Autowired                             // field injection
    private OrderRepository orderRepository;

    @GetMapping("/{id}")
    public Order getOrder(@PathVariable Long id) {    // Long ID instead of UUID
        return orderRepository.findById(id)
            .orElseThrow();                           // returns domain entity, not generated DTO
    }

    @PostMapping
    public ResponseEntity<OrderDto> createOrder(@RequestBody OrderDto dto) {
        // Using generated DTO as both request AND response — spec has separate schemas
        Order order = new Order();
        order.setEmail(dto.getCustomerEmail());
        order.setStatus(dto.getStatus().name());      // mixes generated enum with domain
        orderRepository.save(order);
        return ResponseEntity.ok(dto);                // returns input DTO, not persisted state
    }
}

// Plugin config missing useSpringBoot3=true:
// <configOptions>
//     <useJakartaEe>false</useJakartaEe>              <!-- generates javax.* imports on Spring Boot 3 -->
//     <delegatePattern>false</delegatePattern>         <!-- no delegate — must edit generated code -->
// </configOptions>

// Generated sources committed to git — should be in .gitignore and target/
good-delegate-impl.javaexamples
Source on GitHub
// ✅ GOOD — implements generated delegate, never modifies generated code, maps between models

@Service
@RequiredArgsConstructor
public class OrderApiDelegateImpl implements OrdersApiDelegate {

    private final OrderService orderService;

    @Override
    public ResponseEntity<OrderDto> getOrderById(UUID id) {
        Order order = orderService.findById(id);
        return ResponseEntity.ok(toDto(order));
    }

    @Override
    public ResponseEntity<OrderDto> createOrder(CreateOrderRequestDto request) {
        Order order = orderService.createOrder(
            request.getCustomerEmail(),
            request.getItems().stream()
                .map(i -> new OrderItemCommand(i.getProductId(), i.getQuantity()))
                .toList());
        return ResponseEntity.status(HttpStatus.CREATED).body(toDto(order));
    }

    @Override
    public ResponseEntity<PageOrderDto> listOrders(Integer page, Integer size) {
        Page<Order> orders = orderService.findAll(PageRequest.of(page, size));
        PageOrderDto dto = new PageOrderDto()
            .content(orders.getContent().stream().map(this::toDto).toList())
            .totalElements(orders.getTotalElements())
            .totalPages(orders.getTotalPages());
        return ResponseEntity.ok(dto);
    }

    // Map domain → generated DTO (never expose domain model via API)
    private OrderDto toDto(Order order) {
        return new OrderDto()
            .id(order.getId())
            .status(OrderDto.StatusEnum.fromValue(order.getStatus().name()))
            .customerEmail(order.getCustomerEmail())
            .createdAt(order.getCreatedAt());
    }
}
openapi.yamltemplates
Source on GitHub
openapi: 3.0.3
info:
  title: Order API
  version: 1.0.0
  description: Order management REST API

servers:
  - url: http://localhost:8080/api/v1

paths:
  /orders:
    get:
      operationId: listOrders
      summary: List orders with pagination
      tags: [Orders]
      parameters:
        - name: page
          in: query
          schema: { type: integer, default: 0 }
        - name: size
          in: query
          schema: { type: integer, default: 20 }
      responses:
        '200':
          description: Paginated list of orders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageOrder'

    post:
      operationId: createOrder
      summary: Create a new order
      tags: [Orders]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
      responses:
        '201':
          description: Order created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '400':
          $ref: '#/components/responses/ValidationError'

  /orders/{id}:
    get:
      operationId: getOrderById
      summary: Get order by ID
      tags: [Orders]
      parameters:
        - $ref: '#/components/parameters/OrderId'
      responses:
        '200':
          description: Order found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      operationId: deleteOrder
      summary: Delete an order
      tags: [Orders]
      parameters:
        - $ref: '#/components/parameters/OrderId'
      responses:
        '204':
          description: Order deleted

components:
  parameters:
    OrderId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid

  schemas:
    Order:
      type: object
      properties:
        id:
          type: string
          format: uuid
        customerEmail:
          type: string
          format: email
        status:
          type: string
          enum: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
        items:
          type: array
          items:
            $ref: '#/components/schemas/OrderItem'
        createdAt:
          type: string
          format: date-time

    OrderItem:
      type: object
      properties:
        productId:
          type: string
          format: uuid
        productName:
          type: string
        quantity:
          type: integer
          minimum: 1
        unitPrice:
          type: number
          format: double

    CreateOrderRequest:
      type: object
      required: [customerEmail, items]
      properties:
        customerEmail:
          type: string
          format: email
        items:
          type: array
          minItems: 1
          items:
            type: object
            required: [productId, quantity]
            properties:
              productId:
                type: string
                format: uuid
              quantity:
                type: integer
                minimum: 1

    PageOrder:
      type: object
      properties:
        content:
          type: array
          items:
            $ref: '#/components/schemas/Order'
        totalElements:
          type: integer
          format: int64
        totalPages:
          type: integer

    ProblemDetail:
      type: object
      properties:
        type:
          type: string
          format: uri
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
          format: uri

  responses:
    NotFound:
      description: Resource not found
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetail'
    ValidationError:
      description: Validation failed
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetail'

Verification scope

Compiled sources
No
Runtime checks
No

Coverage describes checked-in fixtures, not a guarantee for every snippet or project.

Coverage matrix & agent evidence
ADD TO YOUR AGENT

Install Boot 3 skills

claude plugin marketplace add rrezartprebreza/spring-boot-skills
claude plugin install spring-boot-3-skills@spring-boot-skills