Spring REST to OfficeFloor Conversion Reference
This page summarises the mechanical substitutions required to convert a Spring MVC REST application to OfficeFloor REST YAML configuration. Each section shows the Spring pattern on the left and the OfficeFloor equivalent on the right.
Entry point — no change
The Spring Boot application class is unchanged.
// Spring — unchanged in OfficeFloor
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}Endpoint declaration — controller class → YAML file + plain class
| Spring | OfficeFloor |
@RestController + @GetMapping("/owners") on a class |
owners.GET.yml file under officefloor/rest/ |
@PostMapping("/owners") |
owners.POST.yml |
@PutMapping("/owners/{id}") |
owners/{id}.PUT.yml |
@DeleteMapping("/owners/{id}") |
owners/{id}.DELETE.yml |
@GetMapping("/owners/{ownerId}/pets/{petId}") |
owners/{ownerId}/pets/{petId}.GET.yml |
Nested resource paths follow the same directory convention — the YAML file path mirrors the URL structure. Both variables are available to the service method. Use @PathVariable(name = "…") — not the shorthand @PathVariable("…") — for the reason explained in the path parameters section below:
// Spring — nested resource with two path variables
@GetMapping("/owners/{ownerId}/pets/{petId}")
public ResponseEntity<PetDto> getPet(
@PathVariable Integer ownerId,
@PathVariable Integer petId) { ... }
// OfficeFloor — owners/{ownerId}/pets/{petId}.GET.yml
service:
class: com.example.rest.GetPetService
// OfficeFloor — GetPetService.java — note name= attribute, not value shorthand
public class GetPetService {
public void service(
@PathVariable(name = "ownerId") Integer ownerId,
@PathVariable(name = "petId") Integer petId,
ClinicService clinicService,
ObjectResponse<PetDto> response) { ... }
}
A working two-variable example with routing-priority tests is in the Spring REST HTTP Server tutorial.
The service class itself is a plain Java class — no Spring stereotype annotations:
// Spring
@RestController
@RequestMapping("/api")
public class OwnerRestController {
@GetMapping("/owners/{id}")
public ResponseEntity<OwnerDto> getOwner(@PathVariable Integer ownerId) { ... }
}
// OfficeFloor — owners/{ownerId}.GET.yml
service:
class: com.example.rest.GetOwnerService
// OfficeFloor — GetOwnerService.java (plain class, no Spring annotations on the class)
public class GetOwnerService {
public void service(...) { ... }
}Spring bean injection — no annotation needed on bean parameters
Spring beans (@Service, @Repository, @Component, etc.) are injected directly as method parameters by type. No annotation is needed on the parameter itself.
// Spring
@Autowired
private ClinicService clinicService;
// OfficeFloor — ClinicService injected by matching its type
public void service(ClinicService clinicService, ObjectResponse<OwnerDto> response) { ... }HTTP response — ObjectResponse with or without ResponseEntity
The simplest migration keeps ResponseEntity building code intact by wrapping it in ObjectResponse<ResponseEntity<T>>. A deeper conversion replaces it with the OfficeFloor-idiomatic form.
| Spring | OfficeFloor |
ResponseEntity<T> return type |
ObjectResponse<T> method parameter |
return ResponseEntity.ok(dto) |
response.send(dto) (default 200) |
return ResponseEntity.status(201).body(dto) |
ObjectResponse<ResponseEntity<T>> then response.send(ResponseEntity.status(201).body(dto)) — keeps Spring ResponseEntity building; or use @HttpResponse(status = 201) ObjectResponse<T> response for the OfficeFloor-idiomatic form |
ResponseEntity.created(uri).body(dto) (201 + Location header) |
ObjectResponse<ResponseEntity<T>> then response.send(ResponseEntity.created(URI.create("/resource/" + id)).body(dto)) — see the Data JPA tutorial for a worked example |
| custom response headers | ObjectResponse<ResponseEntity<T>> then response.send(ResponseEntity.ok().header("X-Header", value).body(dto)) |
return ResponseEntity.noContent().build() |
omit ObjectResponse parameter entirely — OfficeFloor returns 204 automatically when no response is sent |
| HTTP redirect (302) | ObjectResponse<ResponseEntity<Void>> then response.send(ResponseEntity.status(302).location(URI.create("/target")).build()); or inject HttpServletResponse and call sendRedirect("/target") — see the Servlet Interop tutorial |
Path parameters — use @PathVariable(name = "…") with an explicit name attribute
Spring's @PathVariable works directly in OfficeFloor service methods. Always use the name = attribute form. The shorthand @PathVariable("ownerId") sets the value attribute, which requires @AliasFor synthesis to alias to name; OfficeFloor resolves arguments from raw Java reflection where that synthesis is not applied, so the shorthand produces an empty name and the binding fails at runtime.
// Spring controller
public ResponseEntity<OwnerDto> getOwner(@PathVariable Integer ownerId, ...) { ... }
// OfficeFloor service class — use name= attribute directly; value= shorthand requires
// @AliasFor synthesis which is not applied in OfficeFloor's argument resolution
public void service(@PathVariable(name = "ownerId") Integer ownerId,
ClinicService clinicService,
ObjectResponse<OwnerDto> response) { ... }Query parameters — use @RequestParam with an explicit name
Spring's @RequestParam works directly in OfficeFloor service methods. Always supply name = "..." explicitly for the same reason.
// Spring controller
public ResponseEntity<List<OwnerDto>> listOwners(
@RequestParam(required = false) String lastName, ...) { ... }
// OfficeFloor service class — explicit name required
public void service(@RequestParam(name = "lastName", required = false) String lastName,
ClinicService clinicService,
ObjectResponse<List<OwnerDto>> response) { ... }Request body — no change
@RequestBody works identically in OfficeFloor service methods.
// Spring and OfficeFloor — identical
public void service(@RequestBody OwnerDto ownerDto, ...) { ... }404 / not-found responses — exception + YAML escalation
Spring controllers typically branch on a null check and return ResponseEntity.notFound(). In OfficeFloor the idiomatic approach is to throw a checked exception and handle it in a separate method, wired through YAML escalations:.
// Spring
if (owner == null) return ResponseEntity.notFound().build();
// OfficeFloor service class — two methods, both reachable as separate YAML steps
public void service(@HttpPathParameter("id") String idStr,
ClinicService clinicService,
ObjectResponse<OwnerDto> response) throws NotFoundException {
Owner owner = clinicService.findById(Integer.parseInt(idStr));
if (owner == null) throw new NotFoundException(idStr);
response.send(toDto(owner));
}
public void notFound(@Parameter NotFoundException ex,
@HttpResponse(status = 404) ObjectResponse<String> response) {
response.send(ex.getMessage());
}
// OfficeFloor YAML — method: required because the class has two public methods
service:
class: com.example.rest.GetOwnerService
method: service
escalations:
com.example.exception.NotFoundException: notFound
notFound:
class: com.example.rest.GetOwnerService
method: notFound
See the Exception Handling tutorial for the full picture, including composition-level escalations and @RestControllerAdvice.
Multi-method rule — method: is required
When the service class has more than one public method, OfficeFloor cannot determine which to invoke and the application fails to start:
Require configuring method for service (GetOwnerService) as it contains
multiple public methods (notFound, service)
Every YAML entry that references such a class must include method:. This applies whether the methods are in the same YAML file or in different files.
See the Spring REST HTTP Server tutorial for a worked example using two independent service methods in one class.
Transaction governance — YAML govern: replaces @Transactional
// Spring
@Transactional(readOnly = true)
public Owner findById(int id) { ... }
// OfficeFloor YAML — governs the entire step, not the service bean method
service:
class: com.example.rest.GetOwnerService
method: service
govern: [ readonly-transaction ]
Two built-in governance names are provided by the starter:
readonly-transaction |
Read-only transaction; database driver may skip write-path overhead. |
transaction |
Read/write transaction; rolls back on any exception. |
Multiple steps that all carry govern: [ transaction ] share the same transaction. See the Data JPA tutorial for details.
server.servlet.context-path — supported
Setting server.servlet.context-path in application.properties works as expected. OfficeFloor strips the context path from the request URI before routing, so endpoint YAML files are always named relative to the context root regardless of the configured path.
# application.properties
server.servlet.context-path=/petclinic
# YAML file name — relative to context root, not to the full server path
officefloor/rest/owners.GET.yml → GET /petclinic/owners
officefloor/rest/vets.GET.yml → GET /petclinic/vets@RestControllerAdvice — works without change; global escalation is the OfficeFloor-idiomatic alternative
Spring @RestControllerAdvice / @ExceptionHandler classes handle exceptions that escape the OfficeFloor composition without any extra configuration. They remain the right choice when the same handler must cover both OfficeFloor and native Spring @RestController endpoints.
For applications that have fully migrated to OfficeFloor REST composition, the preferred alternative is a global escalation file. Place a YAML file named after the fully qualified exception class in officefloor/escalation/:
# officefloor/escalation/com.example.exception.NotFoundException.yml
handle:
class: com.example.rest.NotFoundHandler
The handler is a plain class using the same OfficeFloor function injection pattern as all other composition steps:
public class NotFoundHandler {
public void handle(@Parameter NotFoundException ex, ObjectResponse<String> response) {
response.send(ex.getMessage());
}
}
Global escalation takes priority over @RestControllerAdvice for the same exception type. Method and composition escalations declared in individual YAML files take priority over global escalation, so endpoints can always override the application-wide handler when needed.
See the Exception Handling tutorial for a worked example of all four levels.
Pattern quick-reference — where to look
The mechanical substitutions above cover the core conversion. Several Spring annotations have direct OfficeFloor equivalents documented in dedicated tutorials:
| Spring annotation / feature | Tutorial |
|---|---|
@PreAuthorize, @Secured, @RolesAllowed |
Security tutorial |
@CrossOrigin |
CORS tutorial |
@Valid, @Validated, custom @Constraint |
Validation tutorial |
@ExceptionHandler, @ControllerAdvice |
Exception Handling tutorial |
springdoc-openapi, Swagger UI |
OpenAPI tutorial |
@Transactional, Spring Data JPA |
Data JPA tutorial |
Pagination (Page<T>, PageRequest) |
Data JPA tutorial |
ResponseEntity.created(uri) (201 + Location header) |
Data JPA tutorial |
@Aspect / @Around on Spring beans |
No change required — Spring AOP proxies wrap the bean before OfficeFloor injects it, so aspects fire normally |
Conversion checklist
- Replace
@RestController+@*Mappingwith YAML files underofficefloor/rest/. - Convert the controller class to a plain Java class — remove all Spring stereotype annotations.
- Ensure every
@PathVariableuses thename =attribute form —@PathVariable(name = "ownerId"), not the shorthand@PathVariable("ownerId"). The shorthand sets thevalueattribute which requires@AliasForsynthesis; OfficeFloor uses raw reflection where synthesis is not applied and the binding silently fails. - Ensure every
@RequestParamincludes an explicit name —@RequestParam(name = "lastName"). - Replace
ResponseEntity<T> return type with anObjectResponse<T> parameter andresponse.send(dto)for the simple case. When custom headers or a non-200 status are needed, useObjectResponse<ResponseEntity<T> and pass theResponseEntitydirectly toresponse.send(...)— this is also the easiest incremental migration path as it preserves existingResponseEntitybuilding code. - Alternatively, replace
ResponseEntity.status(N)with@HttpResponse(status = N)on theObjectResponseparameter for the OfficeFloor-idiomatic form. - Replace null-check 404 returns with a thrown exception and a YAML
escalations:entry. - Whenever a service class has more than one public method, add
method:to every YAML entry that references it. - Remove
@Transactionalfrom service classes; declaregovern:in the YAML instead. - Verify
@RequestBodyparameters — they work as-is, no changes required. - Replace application-wide
@RestControllerAdviceexception handlers with a YAML file named after the fully qualified exception class underofficefloor/escalation/, pointing to a plain handler class that uses@ParameterandObjectResponse. Keep@RestControllerAdviceonly when the same handler must also cover native Spring@RestControllerendpoints.
Next
Return to the tutorials index to explore the full range of OfficeFloor capabilities.

