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

  1. Replace @RestController + @*Mapping with YAML files under officefloor/rest/.
  2. Convert the controller class to a plain Java class — remove all Spring stereotype annotations.
  3. Ensure every @PathVariable uses the name = attribute form — @PathVariable(name = "ownerId"), not the shorthand @PathVariable("ownerId"). The shorthand sets the value attribute which requires @AliasFor synthesis; OfficeFloor uses raw reflection where synthesis is not applied and the binding silently fails.
  4. Ensure every @RequestParam includes an explicit name — @RequestParam(name = "lastName").
  5. Replace ResponseEntity<T> return type with an ObjectResponse<T> parameter and response.send(dto) for the simple case. When custom headers or a non-200 status are needed, use ObjectResponse<ResponseEntity<T> and pass the ResponseEntity directly to response.send(...) — this is also the easiest incremental migration path as it preserves existing ResponseEntity building code.
  6. Alternatively, replace ResponseEntity.status(N) with @HttpResponse(status = N) on the ObjectResponse parameter for the OfficeFloor-idiomatic form.
  7. Replace null-check 404 returns with a thrown exception and a YAML escalations: entry.
  8. Whenever a service class has more than one public method, add method: to every YAML entry that references it.
  9. Remove @Transactional from service classes; declare govern: in the YAML instead.
  10. Verify @RequestBody parameters — they work as-is, no changes required.
  11. Replace application-wide @RestControllerAdvice exception handlers with a YAML file named after the fully qualified exception class under officefloor/escalation/, pointing to a plain handler class that uses @Parameter and ObjectResponse. Keep @RestControllerAdvice only when the same handler must also cover native Spring @RestController endpoints.

Next

Return to the tutorials index to explore the full range of OfficeFloor capabilities.