Skip to main content

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 brings juneau-rest-server and juneau-marshall with it.
  • org.springframework.boot:spring-boot-starter-web:4.1.1, plus the spring-boot-maven-plugin to 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 PetRootServlet through the ServletRegistrationBean, 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 @Rest paths; see REST Server Self-Registration.
  • Spring beans as children. BasicSpringRestServletGroup resolves each class in children from the Spring context before it tries to create one itself, so PetResource is the petResource bean with PetStore already injected. The module and its auto-configuration classes are described in juneau-rest-server-springboot.
  • Operations. @RestGet and @RestPost map methods to HTTP verbs and paths, @Path binds the {id} segment, and @Content parses the request body into a Pet. Throwing NotFound sends a 404. The same annotations are introduced in Your first REST app.
  • Formats. BasicRestResource carries Juneau's universal configuration, so the Accept header picks the serializer. JSON is Juneau's job here, which is why the pom.xml excludes Spring's Jackson starter.

Next steps​