How to Write APIs Without Writing Controllers
When we usually talk about building APIs, the first thought that comes to mind is writing a controller. We define our routes, map them to methods, and then start coding the logic. That’s the traditional way.
But recently, I came across a different approach that changed how I look at API development — the API First approach.
Here’s how it works:
👉 Instead of writing the controller first, you define your API specification (for example, using OpenAPI/Swagger). This specification becomes the source of truth.
👉 From this spec, the framework generates the boilerplate code for you — including the controller.
👉 Along with the controller, something very interesting gets generated: a delegate interface. This is like a contract, where every API endpoint corresponds to a method in the delegate.
👉 As a developer, your job is not to write the controller anymore. Instead, you just implement this delegate interface. You write the actual business logic inside those methods.
⚙️ Example with Spring Boot and Maven
Spring Boot provides a Maven plugin that makes this super easy. Here’s how you can use it:
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.0.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
<generatorName>spring</generatorName>
<output>${project.build.directory}/generated-sources</output>
</configuration>
</execution>
</executions>
</plugin>
mvn clean install
This generates the controller and delegate interface in the target/generated-sources folder.
📄 Example of a Generated Class (from OpenAPI)
Let’s say your openapi.yaml defines a GET /users/{id} endpoint. The generator creates two key classes:
Recommended by LinkedIn
Generated Controller (UserApi.java)
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen")
@RestController
@RequestMapping("/api")
public interface UserApi {
@GetMapping("/users/{id}")
ResponseEntity<User> getUserById(@PathVariable("id") Long id);
}
Generated Delegate Interface (UserApiDelegate.java)
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen")
public interface UserApiDelegate {
default ResponseEntity<User> getUserById(Long id) {
// default implementation (can return 501 Not Implemented)
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
}
Your job: implement the delegate.
@Service
public class UserApiDelegateImpl implements UserApiDelegate {
@Override
public ResponseEntity<User> getUserById(Long id) {
User user = new User(id, "Rajjo");
return ResponseEntity.ok(user);
}
}
Notice how the heavy lifting (annotations, mappings, boilerplate) is already handled by the generated code. You only focus on logic.
🤝 Bonus: Mock APIs for Frontend Teams
One hidden superpower of this approach is how useful it is for mock APIs. Since everything starts from the OpenAPI spec, you can:
For example:
@Override
public ResponseEntity<User> getUserById(Long id) {
// Mock response for frontend testing
User mockUser = new User(id, "Test User");
return ResponseEntity.ok(mockUser);
}
This way, frontend teams don’t have to wait for backend development to finish. They can work in parallel, using the same API contract that will be used in production.
🔑 Why this excites me:
✨ Have you worked with this API First approach (maybe with Spring Boot) before? How was your experience?