Build a REST API on Spring Boot
What you'll build
A small pet API that runs on Spring Boot's embedded Tomcat. It lists pets, returns one pet by id and adds a
pet, and the same methods answer in JSON, XML, HTML and the other formats Juneau registers. The pet store is
an ordinary Spring bean that Spring injects into the Juneau resource. When you are done,
curl -H "Accept: application/json" http://localhost:8080/pets/1 returns {"id":1,"name":"Fido"}.
Prerequisites and time
- Java 17 or newer and Maven 3.9 or newer.
org.apache.juneau:juneau-rest-server-springboot:10.0.0-SNAPSHOT, which bringsjuneau-rest-serverandjuneau-marshallwith it.org.springframework.boot:spring-boot-starter-web:4.1.1, plus thespring-boot-maven-pluginto run the app.- Port 8080 free.
- About 15 minutes.
10.0.0-SNAPSHOT needs the Apache snapshots repository, which the pom.xml below declares; Maven Central has
only the 9.2.0 release today (see Install Juneau and Downloads).
Steps
1. Create the project
Make an empty directory and save this as pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>pets</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<juneau.version>10.0.0-SNAPSHOT</juneau.version>
<spring-boot.version>4.1.1</spring-boot.version>
</properties>
<repositories>
<repository>
<id>apache-snapshots</id>
<url>https://repository.apache.org/snapshots/</url>
<releases><enabled>false</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-springboot</artifactId>
<version>${juneau.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
<exclusions>
<!-- Juneau does the JSON, so Jackson is not needed. -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jackson</artifactId>
</exclusion>
<!-- Spring Boot then logs through java.util.logging. -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>
Put the Java files from the next steps in src/main/java/com/example/pets/, and start each one with
package com.example.pets;.
2. Write the bean
Pet.java is a plain bean. Juneau serializes its public fields, so it needs no annotations:
/**
* A pet, serialized as {@code {"id":1,"name":"Fido"}}.
*/
public class Pet {
/** The pet's id, assigned by {@link PetStore}. */
public int id;
/** The pet's name. */
public String name;
/** Used by the parser. */
public Pet() {}
/**
* Creates a pet.
*
* @param id The id.
* @param name The name.
*/
public Pet(int id, String name) {
this.id = id;
this.name = name;
}
}
3. Add a store
PetStore.java keeps the pets in memory. Spring creates one instance and shares it:
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
/**
* An in-memory pet store. Spring creates one and injects it into {@link PetResource}.
*/
public class PetStore {
private final Map<Integer,Pet> pets = new ConcurrentSkipListMap<>();
private final AtomicInteger nextId = new AtomicInteger(1);
/** Creates the store with one pet in it. */
public PetStore() {
add("Fido");
}
/**
* Adds a pet.
*
* @param name The pet's name.
* @return The new pet, with its id.
*/
public Pet add(String name) {
var pet = new Pet(nextId.getAndIncrement(), name);
pets.put(pet.id, pet);
return pet;
}
/**
* Looks up a pet.
*
* @param id The id.
* @return The pet, or empty if there is none with that id.
*/
public Optional<Pet> get(int id) {
return Optional.ofNullable(pets.get(id));
}
/**
* Returns every pet, in id order.
*
* @return The pets.
*/
public List<Pet> list() {
return new ArrayList<>(pets.values());
}
}
4. Write the resource
PetResource.java is the API. It extends BasicRestResource, the base class for child resources, and takes
the store in its constructor:
import java.util.*;
import org.apache.juneau.http.*;
import org.apache.juneau.http.response.*;
import org.apache.juneau.rest.server.*;
import org.apache.juneau.rest.server.servlet.*;
import org.springframework.beans.factory.annotation.*;
/**
* The pet API, mounted at {@code /pets} under {@link PetRootServlet}.
*/
@Rest(path = "/pets", title = "Pets")
public class PetResource extends BasicRestResource {
private final PetStore store;
/**
* Called by Spring.
*
* @param store The pet store bean.
*/
@Autowired
public PetResource(PetStore store) {
this.store = store;
}
/**
* Lists the pets.
*
* @return Every pet.
*/
@RestGet("/")
public List<Pet> listPets() {
return store.list();
}
/**
* Returns one pet.
*
* @param id The pet id from the URL.
* @return The pet.
* @throws NotFound If there is no pet with that id.
*/
@RestGet("/{id}")
public Pet getPet(@Path("id") int id) throws NotFound {
return store.get(id).orElseThrow(() -> new NotFound("No pet with id " + id));
}
/**
* Adds a pet.
*
* @param pet The pet to add; only {@code name} is used.
* @return The new pet, with its id.
*/
@RestPost("/")
public Pet addPet(@Content Pet pet) {
return store.add(pet.name);
}
}
5. Add the root servlet
PetRootServlet.java is the servlet Spring Boot mounts. It extends BasicSpringRestServletGroup, so its
children are looked up as Spring beans:
import org.apache.juneau.rest.server.*;
import org.apache.juneau.rest.server.springboot.*;
/**
* The servlet Spring Boot mounts. Its children are looked up as Spring beans.
*/
@Rest(title = "Pet service", children = { PetResource.class })
public class PetRootServlet extends BasicSpringRestServletGroup {
private static final long serialVersionUID = 1L;
}
6. Wire it into Spring Boot
PetApplication.java declares the beans and mounts the root servlet at /*:
import org.springframework.boot.autoconfigure.*;
import org.springframework.boot.builder.*;
import org.springframework.boot.web.servlet.*;
import org.springframework.context.annotation.*;
/**
* Starts Spring Boot with the pet API on embedded Tomcat (port 8080 by default).
*/
@SpringBootApplication
public class PetApplication {
/**
* Starts the application.
*
* @param args Spring Boot arguments, for example {@code --server.port=9090}.
*/
public static void main(String[] args) {
new SpringApplicationBuilder(PetApplication.class).run(args);
}
/**
* The store shared by every request.
*
* @return A new store.
*/
@Bean
public PetStore petStore() {
return new PetStore();
}
/**
* The pet API, as a Spring bean so the store is injected.
*
* @param store The store bean.
* @return The resource.
*/
@Bean
public PetResource petResource(PetStore store) {
return new PetResource(store);
}
/**
* The root servlet.
*
* @return The servlet.
*/
@Bean
public PetRootServlet petRootServlet() {
return new PetRootServlet();
}
/**
* Mounts the root servlet at {@code /*}, so the API answers at {@code /pets}.
*
* @param servlet The root servlet bean.
* @return The registration.
*/
@Bean
public ServletRegistrationBean<PetRootServlet> petServlet(PetRootServlet servlet) {
return new ServletRegistrationBean<>(servlet, "/*");
}
}
Run it
Start the app:
mvn spring-boot:run
When the log shows Tomcat started on port 8080, call it from a second terminal:
curl -H "Accept: application/json" http://localhost:8080/pets/1
{"id":1,"name":"Fido"}
Add a pet:
curl -H "Accept: application/json" -H "Content-Type: application/json" -d '{"name":"Rex"}' http://localhost:8080/pets
{"id":2,"name":"Rex"}
List them:
curl -H "Accept: application/json" http://localhost:8080/pets
[{"id":1,"name":"Fido"},{"id":2,"name":"Rex"}]
Ask for a pet that does not exist and print only the status code:
curl -s -o /dev/null -w "%{http_code}\n" -H "Accept: application/json" http://localhost:8080/pets/99
404
Leave out the Accept header, or open http://localhost:8080/pets in a browser, and you get the HTML view
of the same data. Press Ctrl+C to stop the app.
How it works
- Mounting. Spring Boot registers
PetRootServletthrough theServletRegistrationBean, the same pattern Spring Boot Overview uses. If you would rather not write that bean, the opt-in auto-configuration (juneau.rest.auto-register=true) mounts servlets from their own@Restpaths; see REST Server Self-Registration. - Spring beans as children.
BasicSpringRestServletGroupresolves each class inchildrenfrom the Spring context before it tries to create one itself, soPetResourceis thepetResourcebean withPetStorealready injected. The module and its auto-configuration classes are described in juneau-rest-server-springboot. - Operations.
@RestGetand@RestPostmap methods to HTTP verbs and paths,@Pathbinds the{id}segment, and@Contentparses the request body into aPet. ThrowingNotFoundsends a 404. The same annotations are introduced in Your first REST app. - Formats.
BasicRestResourcecarries Juneau's universal configuration, so theAcceptheader picks the serializer. JSON is Juneau's job here, which is why thepom.xmlexcludes Spring's Jackson starter.
Next steps
- Test an endpoint with MockRestClient: test resources like this one without starting a server.
- Secure an endpoint with JWT and OAuth: put a guard in front of the API.
- Generate OpenAPI docs for your API: publish a description of it.