Building robust and scalable REST APIs is a common requirement for modern web development. This application will explore how to create REST APIs for CRUD operations using Spring Boot framework. Apart from building simple REST APIs, we will learn about request validation, error handling, testing, API documentation, and deployment.
Step 1. Create a New Project
We can choose either between Maven or Gradle for dependency management. To create a project using Maven, we can run the following command in the command prompt:
mvn archetype:generate -DgroupId=com.favtuts -DartifactId=spring-boot-rest-example -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
The project’s pom.xml include the following dependencies:
spring-boot-starter-web
: enables creating MVC applications, including REST APIs.spring-boot-starter-data-jpa
: enables persistence and database operations.spring-boot-starter-test
: enables unit testing of the application with JUnit 5.
We can, optionally, add the h2 and Lombok dependencies if we want to use in-memory database and annotation processing.
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.1</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
The same project can be created in Gradle as follows:
$ gradle init --type java-application --dsl groovy --project-name rest-api-crud-gradle Select test framework: 1: JUnit 4 2: TestNG 3: Spock 4: JUnit Jupiter Enter selection (default: JUnit Jupiter) [1..4] Source package (default: rest.api.crud.gradle): com.favtuts.sb.restapi Enter target version of Java (min. 7) (default: 11): 17 Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no] > Task :init To learn more about Gradle by exploring our Samples at https://docs.gradle.org/8.3/samples/sample_building_java_applications.html BUILD SUCCESSFUL in 4m 23s 2 actionable tasks: 2 executed $ ./gradlew app:run > Task :app:run Hello World!
And we can add the dependencies as follows, or you can check the exactly versions on the website: https://plugins.gradle.org/
plugins { // Apply the application plugin to add support for building a CLI application in Java. id 'application' // Spring Boot Gradle Plugin: create standalone production gradle Spring based Application that you can "just run". id 'org.springframework.boot' version '3.1.3' // A Gradle plugin that provides Maven-like dependency management functionality id "io.spring.dependency-management" version "1.1.3" id 'java' id 'eclipse' id 'idea' } group = 'com.favtuts' version = '0.0.1-SNAPSHOT' sourceCompatibility = '17' targetCompatibility = '17' repositories { // Use Maven Central for resolving dependencies. mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' } // Apply a specific Java toolchain to ease working on different environments. java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } application { // Define the main class for the application. mainClass = 'com.favtuts.sb.restapi.App' }
Another approach is to use the Spring Initializr online tool and choose the required dependencies, build tool and Java runtime. It will generate the project in zip file, which we can download and import in an IDE.

Step 2. Create Model and Configure Persistence
In REST, the data is modeled into resource representations. It is generally the JSON or XML document received when the REST API is invoked over HTTP protocol. In Java, the resource presentation can be any plain Java object that has fields, setters, getters and constructors. We can also override the hashCode()
and equals()
methods if we need to customize the object equality rules.
The following Item class has the @Entity annotation as well so we can persist it into the database using JPA repositories.
@Entity @Data @NoArgsConstructor @AllArgsConstructor public class Item { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; }
Spring Data JPA module has various repository interfaces, such as JpaRepository, that provide the inbuilt methods for CRUD operations on JPA entities.
@Repository public interface ItemRepository extends JpaRepository<Item, Long> { }
In the end, do not forget to add the DataSource properties
that establish a connection to the backend database. In this demo, we are using the H2 database. You can change the properties to connect to another database transparently.
spring.datasource.url=jdbc:h2:mem:test spring.datasource.username=sa spring.datasource.password= spring.datasource.driverClassName=org.h2.Driver spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=update
Step 3. Create REST Resource/Controller
The REST resource exposes the API URLs where the clients can connect to and request CRUD operations. In our example, we are creating the resource for Item class. The ItemController class uses the Spring MVC annotations, such as @RestController
, @GetMapping
and @PostMapping
, to annotate the methods as handler methods for REST resources.
@RestController public class ItemController { @Autowired ItemRepository itemRepository; @GetMapping("/items") List<Item> all() { return itemRepository.findAll(); } @GetMapping("/items/{id}") Item getById(@PathVariable Long id) { return itemRepository.findById(id).get(); } @PostMapping("/items") Item createNew(@RequestBody Item newItem) { return itemRepository.save(newItem); } @DeleteMapping("/items/{id}") void delete(@PathVariable Long id) { itemRepository.deleteById(id); } @PutMapping("/items/{id}") Item updateOrCreate(@RequestBody Item newItem, @PathVariable Long id) { return itemRepository.findById(id) .map(item -> { item.setName(newItem.getName()); return itemRepository.save(item); }) .orElseGet(() -> { newItem.setId(id); return itemRepository.save(newItem); }); } }
Step 4. Configure Error Handling
In the real world, not all the requests will be valid requests. In some cases, the requests will throw exceptions and we need to configure the error-handling mechanism.
For example, if a client requests an Item whose ID does not exist in the database, API must throw the HTTP 404 Not Found
error.
@GetMapping("/items/{id}") Item getById(@PathVariable Long id) { return itemRepository.findById(id) .orElseThrow(() -> new ItemNotFoundException(id)); }
In Spring Boot, we can configure the error handling by configuring the @ControllerAdvice and @ExceptionHandler annotations. Its handler methods will catch the configured exception from any controller in the application and translate the exception into a valid REST response.
public class ItemNotFoundException extends RuntimeException { private Long id; public ItemNotFoundException(Long id) { super("Could not find item " + id); } }
@ControllerAdvice public class ApplicationExceptionHandler { @ExceptionHandler(ItemNotFoundException.class) @ResponseBody @ResponseStatus(HttpStatus.NOT_FOUND) String itemNotFoundHandler(ItemNotFoundException ex) { return ex.getMessage(); } }
Step 5. Configure Request Validation
REST APIs must have a mechanism to cleanse the request body and provide appropriate messages suggesting what is wrong with the input body. This can be done by configuring the Jakarta Validation which seamlessly integrates with the Spring boot applications.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
dependencies { ... implementation 'org.springframework.boot:spring-boot-starter-validation' ... }
To add validation of a request body, annotate the request input object with @Valid
annotation in the handler method.
@PostMapping("/items") Item createNew(@Valid @RequestBody Item newItem) { return itemRepository.save(newItem); }
Next, we can add the JSR 380 annotation in the model class to add the validation rules specific to the fields.
public class Item { ... @NotBlank private String name; }
Testing the API with cURL command:
curl --location 'http://localhost:8080/items' \ --header 'Content-Type: application/json' \ --data '{ "name": "" }'
We can see the response like this:
Status: 400 - Bad request { "timestamp": "2023-09-09T04:34:11.460+00:00", "status": 400, "error": "Bad Request", "path": "/items" }
Checking on the console logs the see the NotBlank
error with MethodArgumentNotValidException
:
2023-09-09T11:34:11.453+07:00 WARN 16098 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in com.favtuts.sb.restapi.model.Item com.favtuts.sb.restapi.web.ItemController.createNew(com.favtuts.sb.restapi.model.Item): [Field error in object 'item' on field 'name': rejected value []; codes [NotBlank.item.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.name,name]; arguments []; default message [name]]; default message [must not be blank]] ]
To show more details about validation erros, we will provide the ErrorRespons model
@XmlRootElement(name = "error") @AllArgsConstructor @NoArgsConstructor @Data public class ErrorResponse { private String message; private List<String> details; }
Then provide the method to render response in case of MethodArgumentNotValidException:
@RestControllerAdvice public class ApplicationExceptionHandler { @ExceptionHandler(ItemNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) String itemNotFoundHandler(ItemNotFoundException ex) { return ex.getMessage(); } @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) { List<String> details = new ArrayList<>(); for (ObjectError error : ex.getBindingResult().getAllErrors()) { details.add(error.getDefaultMessage()); } ErrorResponse error = new ErrorResponse("Validation Failed", details); return new ResponseEntity(error, HttpStatus.BAD_REQUEST); } }
Then you can call the API again:
curl --location 'http://localhost:8080/items' \ --header 'Content-Type: application/json' \ --data '{ "name": "" }'
We can see the more clearer response:
Status: 400 - Bad request { "message": "Validation Failed", "details": [ "must not be blank" ] }
Step 6. API Documentation
Documentation is an essential part of building REST APIs. It helps the clients in learning how to consume the APIs in a consistent manner.
In this example, we are creating the REST API documentation using OpenAPI 3 specification for Spring Boot 3.x applications.
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.1.0</version> </dependency>
dependencies { ... implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' ... }
After setting up the above dependency, we can run our application and find the OpenAPI descriptions at /v3/api-docs, which is the default path:
http://localhost:8080/v3/api-docs
Note that the springdoc-openapi
dependency already includes Swagger UI, so we can interact with our API specification and exercise the endpoints at:
http://localhost:8080/swagger-ui/index.html

Step 7. Build and Deploy the REST API
The most preferred way to build the APIs is creating a JAR file deployment or creating a docker image to deploy as a container for scalability.
By default, the deployment is set to jar in the pom.xml file. We can change it to war if we want to deploy the APIs in an external application server.
<packaging>jar</packaging>
So we can create the deployment build file using the mvn package command.
mvn clean package
./gradlew clean build --info
Step 8. Demo
Let us run the application and see what we have built till now. Run the application from the terminal using the command:
mvn clean spring-boot:run
./gradlew bootRun
Invoke the API URL /items
. It should return the two items that we saved during the application startup:
@SpringBootApplication public class App implements CommandLineRunner { public static void main(String[] args) { SpringApplication.run(App.class, args); } @Autowired ItemRepository itemRepository; @Override public void run(String... args) throws Exception { itemRepository.save(new Item(null, "Item 1")); itemRepository.save(new Item(null, "Item 2")); } }

Next, invoke a few APIs to verify that all the REST APIs are working as desired.

Also, check the error scenarios.

Conclusion
In this Spring boot REST API tutorial, we created APIs for CRUD operations step-by-step, providing explanations and code examples along the way. It provided a clear understanding of how to structure your code, implement CRUD operations, handle validations and errors, and deploy the application.
Happy Learning !!