Creating a RESTful API using Spring Boot
Here’s a comprehensive guide to creating a RESTful API with Spring Boot:
1. Project Setup
- Use Spring Initializr (start.spring.io) to create a new project. This is the project structure we will create:
src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── demo/
│ │ ├── DemoApplication.java
│ │ ├── controller/
│ │ │ └── UserController.java
│ │ ├── model/
│ │ │ └── User.java
│ │ ├── repository/
│ │ │ └── UserRepository.java
│ │ ├── service/
│ │ │ └── UserService.java
│ │ └── exception/
│ │ ├── ResourceNotFoundException.java
│ │ ├── ErrorResponse.java
│ │ └── GlobalExceptionHandler.java
│ └── resources/
│ └── application.properties
└── pom.xml
- Add dependencies: Spring Web, Spring Data JPA, and a database driver (H2 for this example):
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<!-- Spring Web for REST API -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data JPA for database interaction -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Validation for validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- H2 Database (for in-memory testing) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok for reducing boilerplate code -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
- Configure project properties:
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
2. Key Components
- Model (User.java): Defines the data structure
package com.example.demo.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Entity
@Table(name = "Users") // Avoid using "user" as a table name because it is a reserved keyword in SQL
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String email;
}
- Repository (UserRepository.java): Handles database operations
package com.example.demo.repository;
import com.example.demo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Spring Data JPA provides basic CRUD operations
}
- Service (UserService.java): Implements business logic
package com.example.demo.service;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import com.example.demo.exception.ResourceNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> getAllUsers() {
return userRepository.findAll();
}
public Optional<User> getUserById(Long id) {
return userRepository.findById(id);
}
public User createUser(User user) {
return userRepository.save(user);
}
public User updateUser(Long id, User userDetails) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException(
"User not found with id: " + id
));
user.setName(userDetails.getName());
user.setEmail(userDetails.getEmail());
return userRepository.save(user);
}
public void deleteUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
userRepository.delete(user);
}
}
- Controller (UserController.java): Defines REST endpoints which provide these RESTful endpoints:
- GET /api/users: Retrieve all users
- GET /api/users/{id}: Retrieve a specific user
- POST /api/users: Create a new user
- PUT /api/users/{id}: Update an existing user
- DELETE /api/users/{id}: Delete a user
package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
// GET all users
@GetMapping
public List<User> getAllUsers() {
return userService.getAllUsers();
}
// GET user by ID
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.getUserById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// CREATE a new user
@PostMapping
public User createUser(@RequestBody User user) {
return userService.createUser(user);
}
// UPDATE an existing user
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(
@PathVariable Long id,
@Valid @RequestBody User userDetails
) {
User updatedUser = userService.updateUser(id, userDetails);
return ResponseEntity.ok(updatedUser);
}
// DELETE a user
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.ok().build();
}
}
- Exception Handling:
-
Custom Exception for Resource Not Found
package com.example.demo.exception; public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String message) { super(message); } }
-
Global Exception Handler with @ControllerAdvice allows centralized exception handling. Three main exception handlers:
a. ResourceNotFoundException: Handles specific resource-not-found scenarios b. ConstraintViolationException: Manages input validation errors c. Generic Exception handler for unexpected errors
public class GlobalExceptionHandler { // Handle specific ResourceNotFoundException @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<ErrorResponse> handleResourceNotFoundException( ResourceNotFoundException ex, WebRequest request ) { ErrorResponse error = new ErrorResponse( HttpStatus.NOT_FOUND, "Resource Not Found", ex.getMessage() ); return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); } // Handle validation errors @ExceptionHandler(jakarta.validation.ConstraintViolationException.class) public ResponseEntity<ErrorResponse> handleValidationExceptions( jakarta.validation.ConstraintViolationException ex, WebRequest request ) { ErrorResponse error = new ErrorResponse( HttpStatus.BAD_REQUEST, "Validation Error", ex.getMessage() ); return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); } // Handle generic exceptions @ExceptionHandler(Exception.class) public ResponseEntity<ErrorResponse> handleGlobalException( Exception ex, WebRequest request ) { ErrorResponse error = new ErrorResponse( HttpStatus.INTERNAL_SERVER_ERROR, "An unexpected error occurred", ex.getMessage() ); return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); } }
-
Error Response DTO provides a structured error response, includes timestamp, HTTP status, message, and details, helps provide consistent error information:
class ErrorResponse { private LocalDateTime timestamp; private HttpStatus status; private String message; private String details; public ErrorResponse(HttpStatus status, String message, String details) { this.timestamp = LocalDateTime.now(); this.status = status; this.message = message; this.details = details; } // Getters and setters public LocalDateTime getTimestamp() { return timestamp; } public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; } public HttpStatus getStatus() { return status; } public void setStatus(HttpStatus status) { this.status = status; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getDetails() { return details; } public void setDetails(String details) { this.details = details; } }
- Main Method (DemoApplication.java):
// Main Application: DemoApplication.java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
3. Running the Application
- Execute the main method in DemoApplication.java
- The API will be available at http://localhost:8080/api/users
4. Testing the API
We can use tools like Postman or curl to test the endpoints:
- GET http://localhost:8080/api/users
- POST http://localhost:8080/api/users (with JSON body)
- PUT http://localhost:8080/api/users/1 (with JSON body)
- DELETE http://localhost:8080/api/users/1
Key Spring Boot Features Demonstrated:
- @RestController for defining REST endpoints
- @GetMapping, @PostMapping, @PutMapping, @DeleteMapping
- ResponseEntity for flexible HTTP responses
- Spring Data JPA for database operations
- Dependency Injection with @Autowired
- Exception handling with @ControllerAdvice, @ExceptionHandler