API-First Development: REST and gRPC

This section documents Spring Boot 4.1.x and Spring Framework 7.0.x on the Java 17/21+ baseline, as described by the official Spring Boot reference documentation and each sub-project’s own site (Spring Data, Spring for Apache Kafka, Micrometer, Project Reactor, springdoc-openapi, Spring gRPC, and the others listed in this section’s Bibliography) — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Spring Boot and its ecosystem continue to evolve; the examples here target the current 4.1.x / 7.0.x releases.

This section’s bibliography lists the reference material consulted while preparing these pages.

API-first (also called contract-first or spec-driven) development flips the usual order: the API contract — an OpenAPI YAML document or a .proto file — is written and agreed on before any server code exists, and the interfaces, DTOs, or stubs that the application implements are generated from that contract by a Maven plugin at build time. This page covers contract-first tooling for both REST and gRPC, and contrasts it with the code-first approach (annotations on hand-written classes, with the spec generated from them) used by the sibling REST APIs and gRPC APIs pages, which instead focus on the runtime/framework side of building these services.

Contract-first vs. code-first

Contract-first (this page) Code-first (rest-apis.adoc / grpc-apis.adoc)

Source of truth

The .yaml / .proto file, hand-written and reviewed like code

Java classes and annotations (@RestController, @Service gRPC base classes)

Generated artifact

Server interfaces, DTOs, or gRPC stubs, produced at build time

OpenAPI document / reflection metadata, produced at runtime or build time from the code

Best when

Multiple teams or languages must agree on a stable contract before implementation starts; the contract is published externally

A single team iterates quickly and the contract can follow the code

Typical tool

openapi-generator-maven-plugin, protobuf-maven-plugin

springdoc-openapi, grpc-spring-boot-starter reflection service

The two approaches are not mutually exclusive: a common pattern is contract-first for externally published REST APIs (so consumers get a stable, reviewable spec) combined with springdoc-openapi on internal or auxiliary endpoints that do not warrant a hand-maintained spec. Both can even coexist on the same application, as shown later on this page.

Contract-first REST with the OpenAPI Specification

The contract is a YAML (or JSON) document following the OpenAPI Specification — paths, operations, request/response schemas, and reusable components. Keep it under version control alongside (or ahead of) the service implementation, and review changes to it the same way you would review a Java interface change.

Organizing the spec

For anything beyond a toy API, split the document into multiple files under src/main/resources/openapi/ and reference them with $ref, rather than maintaining one large monolithic file:

# src/main/resources/openapi/orders-api.yaml
openapi: 3.0.3
info:
  title: Orders API
  version: 1.0.0
paths:
  /orders/{orderId}:
    get:
      operationId: getOrder
      tags: [Orders]
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: The requested order
          content:
            application/json:
              schema:
                $ref: "./schemas/order.yaml#/Order"
        "404":
          description: Order not found
components:
  schemas:
    Order:
      $ref: "./schemas/order.yaml#/Order"
# src/main/resources/openapi/schemas/order.yaml
Order:
  type: object
  required: [id, status, totalAmount]
  properties:
    id:
      type: string
      format: uuid
    status:
      type: string
      enum: [CREATED, PAID, SHIPPED, CANCELLED]
    totalAmount:
      type: number
      format: double

Generating server interfaces and DTOs

OpenAPI Generator reads the spec at build time and emits Java interfaces, model (DTO) classes, and Spring MVC annotations, via openapi-generator-maven-plugin:

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>7.9.0</version>
    <executions>
        <execution>
            <id>generate-orders-api</id>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>${project.basedir}/src/main/resources/openapi/orders-api.yaml</inputSpec>
                <generatorName>spring</generatorName>
                <output>${project.build.directory}/generated-sources/openapi</output>
                <apiPackage>com.example.orders.api</apiPackage>
                <modelPackage>com.example.orders.api.model</modelPackage>
                <configOptions>
                    <interfaceOnly>true</interfaceOnly>
                    <useSpringBoot3>true</useSpringBoot3>
                    <useTags>true</useTags>
                    <skipDefaultInterface>true</skipDefaultInterface>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>

With interfaceOnly=true, the plugin only generates the OrdersApi interface (with @RequestMapping-annotated methods) and the Order DTO — not a controller — so a hand-written class implements it and contains the actual business logic:

@RestController
public class OrdersController implements OrdersApi {

    private final OrderService orderService;

    public OrdersController(OrderService orderService) {
        this.orderService = orderService;
    }

    @Override
    public ResponseEntity<Order> getOrder(UUID orderId) {
        return orderService.findById(orderId)
                .map(ResponseEntity::ok)
                .orElseGet(() -> ResponseEntity.notFound().build());
    }
}

The compiler now enforces the contract: if the spec changes (a new required field, a renamed operation), the generated interface changes too, and OrdersController fails to compile until it is updated to match — turning contract drift into a build-time error instead of a runtime surprise. Because the generated sources live under target/generated-sources, add that directory to the IDE’s source roots (most IDEs pick it up automatically from the Maven build) and never hand-edit generated files.

springdoc-openapi: the code-first complement

springdoc-openapi takes the opposite direction: it inspects the running application’s @RestController classes, request/response types, and Bean Validation annotations, and produces an OpenAPI document and a Swagger UI from the code, rather than generating code from a document.

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.8.6</version>
</dependency>
@RestController
@RequestMapping("/reports")
@Tag(name = "Reports", description = "Ad-hoc reporting endpoints")
public class ReportController {

    @GetMapping("/{reportId}/summary")
    @Operation(summary = "Fetch a summary for the given report")
    @ApiResponse(responseCode = "200", description = "Summary computed")
    @ApiResponse(responseCode = "404", description = "Report not found")
    public ResponseEntity<ReportSummary> getSummary(@PathVariable String reportId) {
        return reportService.summarize(reportId)
                .map(ResponseEntity::ok)
                .orElseGet(() -> ResponseEntity.notFound().build());
    }
}

With no further configuration, the generated document is served at /v3/api-docs and Swagger UI at /swagger-ui.html. @Operation, @ApiResponse, @Parameter, and @Schema refine the description beyond what reflection alone can infer.

Using contract-first and code-first together

Because springdoc-openapi only scans annotated controllers, it happily coexists with a contract-first module on the same application: the generated OrdersController (implementing the OpenAPI-Generator interface) is picked up by springdoc’s reflection scan exactly like any other @RestController, so Swagger UI ends up documenting both the contract-first endpoints and any code-first ones side by side — useful when a stable, externally published contract governs the public API while internal or auxiliary endpoints are added ad hoc without maintaining a spec for them.

Contract-first gRPC with Protocol Buffers

The gRPC equivalent of an OpenAPI document is a .proto file written against Protocol Buffers: it defines the service’s RPCs and message shapes in a language-neutral schema that generates client and server stubs for any supported language, not just Java.

Writing the service definition

syntax = "proto3";

package com.example.orders.v1;

option java_package = "com.example.orders.grpc";
option java_multiple_files = true;

service OrdersService {
  rpc GetOrder (GetOrderRequest) returns (OrderReply);
  rpc ListOrders (ListOrdersRequest) returns (stream OrderReply);
}

message GetOrderRequest {
  string order_id = 1;
}

message ListOrdersRequest {
  string customer_id = 1;
  int32 page_size = 2;
}

message OrderReply {
  string order_id = 1;
  OrderStatus status = 2;
  double total_amount = 3;
}

enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  CREATED = 1;
  PAID = 2;
  SHIPPED = 3;
  CANCELLED = 4;
}

GetOrder is a simple unary RPC; ListOrders returns a server-streaming response (stream OrderReply), useful for paged or long-running result sets without a separate polling endpoint.

Generating stubs with protobuf-maven-plugin

The protobuf-maven-plugin invokes protoc (and the gRPC Java codegen plugin) during the build to turn .proto files under src/main/proto/ into Java message classes, builders, and gRPC service base classes:

<plugin>
    <groupId>org.xolstice.maven.plugins</groupId>
    <artifactId>protobuf-maven-plugin</artifactId>
    <version>0.6.1</version>
    <configuration>
        <protocArtifact>com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier}</protocArtifact>
        <pluginId>grpc-java</pluginId>
        <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.68.1:exe:${os.detected.classifier}</pluginArtifact>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
                <goal>compile-custom</goal>
            </goals>
        </execution>
    </executions>
</plugin>

compile generates the message types; compile-custom (paired with pluginId=grpc-java) generates the OrdersServiceGrpc.OrdersServiceImplBase abstract class that a hand-written service extends:

@GrpcService
public class OrdersGrpcService extends OrdersServiceGrpc.OrdersServiceImplBase {

    private final OrderService orderService;

    public OrdersGrpcService(OrderService orderService) {
        this.orderService = orderService;
    }

    @Override
    public void getOrder(GetOrderRequest request, StreamObserver<OrderReply> responseObserver) {
        orderService.findById(UUID.fromString(request.getOrderId())).ifPresentOrElse(
                order -> {
                    responseObserver.onNext(toReply(order));
                    responseObserver.onCompleted();
                },
                () -> responseObserver.onError(
                        Status.NOT_FOUND.withDescription("Order not found").asRuntimeException()));
    }
}

As with the REST generator, the generated ImplBase class changes whenever the .proto contract changes, so a service that no longer overrides a renamed or removed RPC correctly fails to compile rather than silently drifting from the contract.

Publishing an HTML reference with protoc-gen-doc

Unlike OpenAPI, a .proto file has no built-in Swagger-UI-style renderer; protoc-gen-doc is a separate protoc plugin that turns the same .proto sources into an HTML (or Markdown/JSON) reference page for human readers, and can be wired into the same Maven build as an additional protoc plugin invocation:

<plugin>
    <groupId>org.xolstice.maven.plugins</groupId>
    <artifactId>protobuf-maven-plugin</artifactId>
    <version>0.6.1</version>
    <configuration>
        <protocArtifact>com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier}</protocArtifact>
        <protocPlugins>
            <protocPlugin>
                <id>doc</id>
                <groupId>io.github.pseudomuto</groupId>
                <artifactId>protoc-gen-doc</artifactId>
                <version>1.5.1</version>
                <mainClass>none</mainClass>
                <!-- protoc-gen-doc ships as a native protoc plugin binary, not a Java main class;
                     see the plugin's own documentation for the exact artifact classifier to resolve. -->
            </protocPlugin>
        </protocPlugins>
        <outputDirectory>${project.build.directory}/generated-docs</outputDirectory>
        <outputOptions>html,index.html</outputOptions>
    </configuration>
</plugin>

The resulting index.html documents every service, RPC, message, field, and enum with the comments written in the .proto file, giving API consumers a browsable reference comparable to Swagger UI for REST — publish it alongside the built application, e.g. as a static site artifact, so consumers do not need to open the .proto sources directly.

From spec to implementation

Both flows share the same shape: a hand-written contract, a Maven code-generation plugin bound to a build phase, generated interfaces or stubs that the compiler enforces, and a hand-written class that implements them.

flowchart LR subgraph REST[Contract-first REST] A1[orders-api.yaml] --> B1[openapi-generator-maven-plugin] B1 --> C1[OrdersApi interface + Order DTO] C1 --> D1[OrdersController implements OrdersApi] end subgraph GRPC[Contract-first gRPC] A2[orders.proto] --> B2[protobuf-maven-plugin] B2 --> C2[OrdersServiceImplBase + message classes] C2 --> D2[OrdersGrpcService extends OrdersServiceImplBase] end

Both generation steps run automatically during mvn generate-sources (openapi-generator-maven-plugin’s generate goal and protobuf-maven-plugin’s compile / compile-custom goals default to that phase), so a regular mvn package keeps the generated code in sync with the checked-in contract on every build, and CI fails loudly the moment the implementation drifts from it.

Choosing an approach per API

  • Use contract-first REST (openapi-generator-maven-plugin) for externally published APIs, APIs consumed by multiple independent client teams, or anywhere the spec itself is a deliverable that must be reviewed and versioned like an interface.

  • Use springdoc-openapi (code-first) for internal APIs, admin/reporting endpoints, or anywhere iteration speed matters more than a hand-curated contract — and freely combine it with a contract-first module in the same application, since it documents whatever @RestController beans it finds regardless of how they came to exist.

  • Use contract-first gRPC (protobuf-maven-plugin) whenever the API is consumed by multiple languages, needs streaming semantics, or benefits from the smaller wire format and stricter schema evolution rules that Protocol Buffers provide over JSON; see gRPC APIs for the Spring Boot runtime side (service registration, interceptors, server configuration) once the stubs exist, and REST APIs for the equivalent runtime concerns on the REST side.

For either protocol, treat the contract file (.yaml or .proto) as the reviewed, version-controlled artifact that changes trigger a build failure against — that discipline is the entire point of going contract-first.