Spring REST HTTP Server Tutorial
This tutorial covers the YAML endpoint model in depth: naming conventions, multi-method service classes, multiple path variables, and custom response headers. It assumes you have already completed the Getting Started tutorial.
The tutorial exposes these endpoints:
GET /greetingreturns{"message":"Hello, World!"}using a Spring beanGET /greeting/{name}returns a personalised greeting using a path parameterGET /greeting/formal/{name}andGET /greeting/casual/{name}served by two methods of the same class, illustrating themethod:keyGET /greeting/entity/{name}returns custom response headers viaResponseEntityGET /greeting/{style}/{name}demonstrates two path variables in one endpoint
Maven dependency
The starter is published to Maven Central, so no additional repository configuration is needed. Adding a single dependency to pom.xml is all that is required:
<dependency>
<groupId>net.officefloor.springboot</groupId>
<artifactId>officefloor-rest-spring-boot-4-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
The officefloor-rest-spring-boot-starter auto-configures OfficeFloor into the Spring MVC pipeline. On start-up it scans the classpath for YAML files under officefloor/rest/ and registers each one as a HandlerInterceptor for the corresponding HTTP method and URL path. No additional Java or XML configuration is needed.
Application class
The application entry point is a standard Spring Boot class:
@SpringBootApplication
public class SpringRestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringRestApplication.class, args);
}
}
YAML endpoint files — path and method from the file name
Endpoints are declared as YAML files placed under src/main/resources/officefloor/rest/. The file name encodes both the HTTP method and the URL path:
officefloor/rest/
├── greeting.GET.yml → GET /greeting
└── greeting/
├── {name}.GET.yml → GET /greeting/{name}
├── entity/
│ └── {name}.GET.yml → GET /greeting/entity/{name}
├── formal/
│ └── {name}.GET.yml → GET /greeting/formal/{name}
├── casual/
│ └── {name}.GET.yml → GET /greeting/casual/{name}
└── {style}/
└── {name}.GET.yml → GET /greeting/{style}/{name}
The naming convention is {path}.{METHOD}.yml. Directory structure below officefloor/rest/ becomes the URL path, so deeper URLs are produced simply by nesting files in sub-directories. Curly-brace segments such as {name} become URL path parameters. The special filename index.{METHOD}.yml maps to the root path /.
YAML endpoint files — entries are named steps
Inside each YAML file, entries are named steps. The label on each entry is a developer-chosen name used to wire steps together — it is not a keyword. A step entry identifies the Java class that implements it:
myLabel:
class: com.example.MyLogic
The first entry in the file is always called when the HTTP request arrives.
When a class has only one public method that method is used automatically.
Multi-method classes — method: is required
When a class has more than one public method, OfficeFloor cannot determine which to call and the application fails to start:
Require configuring method for service (GreetingStyleLogic) as it contains
multiple public methods (casual, formal)
Every YAML entry that references such a class must include method: to name which method to invoke.
GreetingStyleLogic is a single class with two independent greeting styles, each serving its own endpoint:
public class GreetingStyleLogic {
public void formal(@PathVariable(name = "name") String name,
ObjectResponse<GreetingResponse> response) {
response.send(new GreetingResponse("Good day, " + name + "."));
}
public void casual(@PathVariable(name = "name") String name,
ObjectResponse<GreetingResponse> response) {
response.send(new GreetingResponse("Hey, " + name + "!"));
}
}
The formal endpoint YAML specifies method: formal:
service:
class: net.officefloor.tutorial.springresthttpserver.GreetingStyleLogic
method: formal
The casual endpoint specifies method: casual:
service:
class: net.officefloor.tutorial.springresthttpserver.GreetingStyleLogic
method: casual
Both entries reference the same class but each picks a different method. The rule holds regardless of how many entries reference the class or whether those entries appear in one YAML file or several.
Spring bean injection — parameters injected by type
The GreetingService is a plain Spring @Service:
@Service
public class GreetingService {
public String greet(String name) {
return "Hello, " + name + "!";
}
}
OfficeFloor registers every bean in the Spring application context as a managed object. Any parameter of a service method whose type matches a Spring bean is injected automatically — no annotation is needed on that parameter.
The handler for GET /greeting receives the bean and uses ObjectResponse to write the JSON response:
public class GetGreetingLogic {
public void service(GreetingService greetingService, ObjectResponse<GreetingResponse> response) {
response.send(new GreetingResponse(greetingService.greet("World")));
}
}
ObjectResponse<T> serialises the object to JSON and writes it to the HTTP response.
ObjectResponse with ResponseEntity — Spring-compatible headers and status
For full Spring compatibility, ObjectResponse also accepts a ResponseEntity<T> as its type parameter. This lets you set custom response headers or a non-200 status code while keeping the same dependency-injected style:
public class GetGreetingEntityLogic {
public void service(
@PathVariable(name = "name") String name,
GreetingService greetingService,
ObjectResponse<ResponseEntity<GreetingResponse>> response) {
response.send(ResponseEntity.ok()
.header("X-Greeting-Name", name)
.body(new GreetingResponse(greetingService.greet(name))));
}
}
Using ObjectResponse<ResponseEntity<GreetingResponse>> gives full control over the HTTP response while staying consistent with the rest of the OfficeFloor programming model. When you only need the body and a 200 status, ObjectResponse<T> is simpler; reach for the ResponseEntity form when you need headers or a specific status code.
Spring annotations on parameters
Service methods can use all the standard Spring MVC parameter annotations. The handler for GET /greeting/{name} uses @PathVariable:
public class GetNamedGreetingLogic {
public void service(
@PathVariable(name = "name") String name,
GreetingService greetingService,
ObjectResponse<GreetingResponse> response) {
response.send(new GreetingResponse(greetingService.greet(name)));
}
}
Both Spring annotations (@PathVariable, @RequestParam, @RequestHeader, @CookieValue, @RequestBody, @ModelAttribute, @RequestPart) and OfficeFloor's own annotations (@HttpPathParameter, @HttpQueryParameter, @HttpHeaderParameter, @HttpObject) are available on any service method parameter.
Note: always use the name = attribute form — @PathVariable(name = "name") and @RequestParam(name = "name"). The shorthand @PathVariable("name") sets the value attribute, which requires @AliasFor annotation synthesis to alias to name; OfficeFloor resolves arguments from raw Java reflection where that synthesis is not applied, so the shorthand silently produces an empty name and the binding fails.
Multiple path variables — nested resource URLs
A URL path with two variable segments — such as GET /greeting/{style}/{name} — is declared by nesting the YAML file inside a variable-named directory:
officefloor/rest/
└── greeting/
└── {style}/
└── {name}.GET.yml → GET /greeting/{style}/{name}
Both variables are available in the service method. Use the name = attribute form on @PathVariable — the shorthand @PathVariable("style") silently fails in OfficeFloor because raw Java reflection does not apply the @AliasFor alias:
service:
class: net.officefloor.tutorial.springresthttpserver.GetStyledGreetingLogic
public class GetStyledGreetingLogic {
public void service(
@PathVariable(name = "style") String style,
@PathVariable(name = "name") String name,
GreetingService greetingService,
ObjectResponse<GreetingResponse> response) {
response.send(new GreetingResponse(style + ": " + greetingService.greet(name)));
}
}
Literal path segments always take priority over variable segments during routing. The test below confirms that GET /greeting/royal/Alice resolves the two-variable endpoint while GET /greeting/formal/Alice still routes to the dedicated greeting/formal/{name}.GET.yml endpoint:
// routes to greeting/{style}/{name}.GET.yml → "royal: Hello, Alice!"
GET /greeting/royal/Alice
// routes to greeting/formal/{name}.GET.yml → "Good day, Alice." (literal wins)
GET /greeting/formal/AliceTesting
@SpringBootTest
@AutoConfigureMockMvc
public class SpringRestHttpServerTest {
@Autowired
private MockMvc mvc;
@Autowired
private ObjectMapper mapper;
@Test
public void getGreeting() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/greeting")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().json(mapper.writeValueAsString(new GreetingResponse("Hello, World!"))));
}
@Test
public void getNamedGreeting() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/greeting/OfficeFloor")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().json(mapper.writeValueAsString(new GreetingResponse("Hello, OfficeFloor!"))));
}
@Test
public void getFormalGreeting() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/greeting/formal/Alice")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().json(mapper.writeValueAsString(new GreetingResponse("Good day, Alice."))));
}
@Test
public void getCasualGreeting() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/greeting/casual/Alice")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().json(mapper.writeValueAsString(new GreetingResponse("Hey, Alice!"))));
}
@Test
public void getGreetingEntity() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/greeting/entity/OfficeFloor")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(header().string("X-Greeting-Name", "OfficeFloor"))
.andExpect(content().json(mapper.writeValueAsString(new GreetingResponse("Hello, OfficeFloor!"))));
}
@Test
public void getStyledGreeting_bothVariablesExtracted() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/greeting/royal/Alice")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().json(mapper.writeValueAsString(
new GreetingResponse("royal: Hello, Alice!"))));
}
@Test
public void getStyledGreeting_literalSegmentWinsOverVariable() throws Exception {
// /greeting/formal/Alice matches greeting/formal/{name}.GET.yml (literal "formal"),
// NOT greeting/{style}/{name}.GET.yml — literal segments take priority over variables
mvc.perform(MockMvcRequestBuilders.get("/greeting/formal/Alice")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().json(mapper.writeValueAsString(
new GreetingResponse("Good day, Alice."))));
}
}
Running against a real server
Because the application is standard Spring Boot, it can be run directly without any test harness:
mvn spring-boot:run
With the server running, the endpoints respond to plain HTTP:
curl http://localhost:8080/greeting
{"message":"Hello, World!"}
curl http://localhost:8080/greeting/OfficeFloor
{"message":"Hello, OfficeFloor!"}
The integration test does the same thing programmatically. @SpringBootTest(webEnvironment = RANDOM_PORT) starts an embedded server on a random port and TestRestTemplate makes real HTTP calls — no MockMvc involved:
@AutoConfigureTestRestTemplate
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SpringRestHttpServerRealServerTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void getGreeting() {
ResponseEntity<GreetingResponse> response = restTemplate.getForEntity(
"/greeting", GreetingResponse.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals(new GreetingResponse("Hello, World!"), response.getBody());
}
@Test
public void getNamedGreeting() {
ResponseEntity<GreetingResponse> response = restTemplate.getForEntity(
"/greeting/OfficeFloor", GreetingResponse.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals(new GreetingResponse("Hello, OfficeFloor!"), response.getBody());
}
@Test
public void getFormalGreeting() {
ResponseEntity<GreetingResponse> response = restTemplate.getForEntity(
"/greeting/formal/Alice", GreetingResponse.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals(new GreetingResponse("Good day, Alice."), response.getBody());
}
@Test
public void getCasualGreeting() {
ResponseEntity<GreetingResponse> response = restTemplate.getForEntity(
"/greeting/casual/Alice", GreetingResponse.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals(new GreetingResponse("Hey, Alice!"), response.getBody());
}
@Test
public void getGreetingEntity() {
ResponseEntity<GreetingResponse> response = restTemplate.getForEntity(
"/greeting/entity/OfficeFloor", GreetingResponse.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals("OfficeFloor", response.getHeaders().getFirst("X-Greeting-Name"));
assertEquals(new GreetingResponse("Hello, OfficeFloor!"), response.getBody());
}
}
Next
The Spring Boot 3 tutorial shows how to add OfficeFloor REST to an existing Spring Boot 3 application and explains which version-specific starter to choose.

