In this post Spring Data tutorial we got to know about Spring Data and how to use it. In this tutorial we'll create a Spring Boot REST API CRUD application using Spring Data JPA and MySQL database. We'll also see how to do exception handling with Spring Boot REST API.
Technologies used
- Java 21
- Spring Boot 3.5.5
- Spring 6.x
- MySQL 8.x
pom.xml
<?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.5.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.netjstech</groupId> <artifactId>userproj</artifactId> <version>0.0.1-SNAPSHOT</version> <name>UserProj</name> <description>User Project with Angular integration</description> <url/> <licenses> <license/> </licenses> <developers> <developer/> </developers> <scm> <connection/> <developerConnection/> <tag/> <url/> </scm> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <!-- MySQL Driver --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
In the configuration, Spring Boot version used is 3.5.5 so Spring Boot gets the dependencies which are supported by this version.
Since we are using Spring Data JPA so spring-boot-starter-data-jpa dependency is added that will get Spring Data, Hibernate and other jars required for JPA.
This is a web application so we add spring-boot-starter-web dependency, that adds the necessary dependencies required for creating a Spring web application. We want to ensure data is there for the fields in the domain obejct so spring-boot-starter-validation is added. This will help with the validation of data by adding Hibernate Validator implementation.
mysql-connector-j dependecy adds the MySQL driver required for the application to connect to DB.
Database table
In the application we are going to have a REST API to create, update and delete user records so we’ll have a USER table in the DB.
CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `first_name` varchar(45) NOT NULL, `last_name` varchar(45) NOT NULL, `user_type` varchar(15) NOT NULL, `start_date` date DEFAULT NULL, PRIMARY KEY (`id`) )
DB Configuration
By default Spring boot reads properties file from this location src/main/resources/application.properties or application.yml file. Create application.yml file at this location and provide the DB connection attributes like URL, driver class name, username, password and Hibernate related properties.
By default Tomcat listens at port 8080, if you want to change port number then provide server.port property otherwise not required.
Change the DB properties as per your database configuration. I have created a schema named netjs that's why url is- jdbc:mysql://localhost:3306/netjs
server: port: 8081 spring: datasource: url: jdbc:mysql://localhost:3306/netjs username: DB_USER password: DB_PASSWORD jpa: properties: hibernate: sqldialect: org.hibernate.dialect.MySQLDialect showsql: true
With these values in properties file and the jars in classpath for Hibernate and MySQL, Spring Boot can automatically configure Hibernate as JPA Vendor and set the DataSource using the DB connection attributes defined in application.yml file.
Entity Class
In the application we’ll have a JPA entity class with fields that map to the columns in the DB table USER.
- @Entity annotation marks this model class as an Entity.
- @Table annotation with the table name specifies the table to which this model class is mapped.
- @Id annotation specifies the primary key of the entity.
- @GeneratedValue annotation specifies the primary key generation strategy which is autoincrement in this case, so you don’t need to send ID data from the application, DB will take care of adding the ID.
- @Column annotation specifies the corresponding table column for the annotated field.
While creating classes, create appropriate packages and create classes in those packages that makes the project more readable, makes component scanning easy from the root package. For example I have put entity class in com.netjstech.datademo.dao.entity package, controller class in com.netjstech.datademo.controller package and so on.
User.java
package com.netjstech.datademo.dao.entity; import java.time.LocalDate; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.validation.constraints.NotBlank; @Entity @Table(name="user") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long userId; @NotBlank(message = "First name is required") @Column(name="first_name") private String firstName; @NotBlank(message = "Last name is required") @Column(name="last_name") private String lastName; @Column(name="user_type") private String userType; @Column(name="start_date") private LocalDate startDate; public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getUserType() { return userType; } public void setUserType(String userType) { this.userType = userType; } public LocalDate getStartDate() { return startDate; } public void setStartDate(LocalDate startDate) { this.startDate = startDate; } }
Spring Data JPA Repository interface
Since we are using Spring Data JPA so you just need to create an interface that extends JpaRepository interface.
JpaRepository is a JPA specific extension of Repository that adds JPA related functionalities like flushing, persistence context along with the CRUD methods inherited from CrudRepository.
package com.netjstech.datademo.dao.repository; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import com.netjstech.datademo.dao.entity.User; public interface UserRepository extends JpaRepository<User, Long> { List<User> findUserByUserType(String userType); }
Here we have created interface UserRepository extending JpaRepository which takes the Entity class and the type of the Id as the type arguments. In our example Entity class is User and type of the Id is Long so these are passed as type arguments.
This interface is the only thing you need to write as your data access code. You don't need to explicitly implement this interface and implement all the CRUD methods. Spring framework automatically implements the operations.
As you can see there is one method added in the interface findUserByUserType(), Spring framework will parse the method and create the implementation for this method too. You don't need to write any implementation or provide SQL query for it.
Controller Class
We’ll create a Controller class UserController.java which is annotated using the @RestController annotation specifying it is a RESTful web service.
package com.netjstech.datademo.controller; import java.net.URI; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import com.netjstech.datademo.dao.entity.User; import com.netjstech.datademo.service.UserService; @RestController @RequestMapping("/user") public class UserController { private final UserService userService; UserController(UserService userService){ this.userService = userService; } // Insert user record @PostMapping @ResponseStatus(HttpStatus.CREATED) public ResponseEntity<User> addUser(@Valid @RequestBody User user) { User createdUser = userService.addUser(user); return ResponseEntity.status(HttpStatus.CREATED).body(createdUser); } // Fetch all user records @GetMapping("/allusers") public ResponseEntity<List<User>> getAllUsers(){ return ResponseEntity.ok(userService.getAllUsers()); } // Fetch single user @GetMapping("/{id}") public ResponseEntity<User> getUserById(@PathVariable("id") int userId){ //System.out.println("ID to find " + userId); User user = userService.getUserById(userId); return ResponseEntity.ok(user); } // Update user record // You can do validation here too @PutMapping public ResponseEntity<User> updateUser(@RequestBody User user) { User updatedUser = userService.updateUser(user); return new ResponseEntity<User>(updatedUser, HttpStatus.OK); } // Delete user record @DeleteMapping("/{id}") public ResponseEntity<Void> deleteUser(@PathVariable int id){ userService.deleteUserById(id); return ResponseEntity.noContent().build(); } @GetMapping("/type") ResponseEntity<?> getUserByType(@RequestParam("type") String userType){ System.out.println("userType " + userType); List<User> users = userService.getAllUsersByUserType(userType); if(users.isEmpty()) { return ResponseEntity.noContent().build(); } return ResponseEntity.ok(users); } }
At the class level there is an annotation @RequestMapping("/user") which means any URL having /user at the end will come to this RestController. Controller class has a dependency on UserService which is injected automatically using constructor injection.
In the controller class there are following methods-
- addUser() annotated with @PostMapping is used to insert a user record. This method has User object as an argument which is annotated with @RequestBody annotation. User object which is passed as HttpRequest body is mapped to the model object by @RequestBody annotation which means this annotation provided automatic deserialization of the HttpRequest body onto a Java object. @Valid annotation ensures data is as per the validations given, which are, first name and last name should not be blank.
- getUsers() annotated with @GetMapping is used to get all the user records.
- getUserById() annotated with @GetMapping("/{id}") is used to fetch specific user record. Id is specified as a path variable which means any URL having the form /user/1 will be intercepted by this method.
- updateUser() annotated with @PutMapping is used to update user record. Return value of the method is of type ResponseEntity and the appropriate HttpStatus code is returned from the method.
- deleteUser() annotated with @DeleteMapping is used to delete the User record whose Id is passed as a path variable.
- getUserByType() method is used to get users belonging to the passed user type. Here @RequestParam is used which means it will match query parameters in the URL. For example in the URL /user/type?type=Gold parameter after the question mark is the query parameter.
Service Class
In the controller class there is a dependency on UserService and controller class just intercepts the request and calls the corresponding service class method. You don’t write any logic in Controller class.
UserService interface
package com.netjstech.datademo.service; import java.util.List; import com.netjstech.model.User; public interface UserService { User addUser(User user); User getUserById(int userId); User updateUser(User user); void deleteUserById(int userId); List<User> getAllUsers(); List<User> getAllUsersByUserType(String userType); }
UserServiceImpl class
From the service layer we’ll call the DAO layer methods. With Spring Data we only need a repository so we’ll call methods of repository from the service class.
To use the repository, UserRepository instance is autowired in the Service class.
Appropriate exception is also thrown from the methods, for example, in updateUser() method it is checked if the passed user is already there in the DB or not by calling findById() method. In case user is not there ResourceNotFoundException is thrown.
package com.netjstech.datademo.service; import java.util.List; import org.springframework.stereotype.Service; import com.netjstech.dao.UserRepository; import com.netjstech.exception.ResourceNotFoundException; import com.netjstech.exception.UserAlreadyExistsException; import com.netjstech.model.User; import jakarta.transaction.Transactional; @Service public class UserServiceImpl implements UserService{ private final UserRepository userRepository; public UserServiceImpl(UserRepository userRepository) { this.userRepository = userRepository; } @Override public User getUserById(int userId) { return userRepository.findById(userId).orElseThrow( () -> new ResourceNotFoundException("User with ID " + userId + " not found")); } @Override public List<User> getAllUsers(){ return userRepository.findAll(); } @Override @Transactional public User updateUser(User user) { // check if the user with the passed id exists or not User userDB = userRepository.findById(user.getUserId()) .orElseThrow(() -> new ResourceNotFoundException("User with ID " + user.getUserId() + " not found")); // Can update only these 3 fields, not start date userDB.setFirstName(user.getFirstName()); userDB.setLastName(user.getLastName()); userDB.setUserType(user.getUserType()); // If user exists then updated return userRepository.save(userDB); } @Override @Transactional public void deleteUserById(int userId) { if (!userRepository.existsById(userId)) { throw new ResourceNotFoundException("User with ID " + userId + " not found"); } System.out.println("Deleting " + userId); userRepository.deleteById(userId); } @Override @Transactional public User addUser(User user) { if (user.getUserId()!= 0 && userRepository.existsById(user.getUserId())) { throw new UserAlreadyExistsException("User with id " + user.getUserId() + " already exists."); } return userRepository.save(user); } @Override public List<User> getAllUsersByUserType(String userType) { return userRepository.findUserByUserType(userType); } }
Spring Boot exception handling using @RestControllerAdvice
In the UserServiceImpl class, ResourceNotFoundException and UserAlreadyExistsException custom exception classes are used.
ResourceNotFoundException
public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String message) { super(message); } }
UserAlreadyExistsException
public class UserAlreadyExistsException extends RuntimeException { public UserAlreadyExistsException(String message) { super(message); } }
When exception is thrown, error response is sent back to the client. For that ErrorResponseDto is created. Note that it is created as a record to avoid boiler plate code like getters/setters,
import java.time.LocalDateTime; import com.fasterxml.jackson.annotation.JsonFormat; public record ErrorResponseDto(int status, @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") LocalDateTime timestamp, String message, String exceptionMessage) {}
A global excpetion handler class annotated with @RestControllerAdvice to handle exceptions.
import java.time.LocalDateTime; import java.util.stream.Collectors; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import com.netjstech.dto.ErrorResponseDto; @RestControllerAdvice public class GlobalExceptionHandler { // For validation failures @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponseDto> handleValidationErrors(MethodArgumentNotValidException ex) { String errorMessage = ex.getBindingResult() .getFieldErrors() .stream() .map(e -> e.getField() + " " + e.getDefaultMessage()) .collect(Collectors.joining(", ")); ErrorResponseDto errorDto = new ErrorResponseDto(HttpStatus.BAD_REQUEST.value(), LocalDateTime.now(), "Validation Failed", errorMessage); return ResponseEntity.badRequest().body(errorDto); } @ExceptionHandler(UserAlreadyExistsException.class) public ResponseEntity<ErrorResponseDto> handleUserExists(UserAlreadyExistsException ex) { ErrorResponseDto errorResponse = new ErrorResponseDto(HttpStatus.CONFLICT.value(), LocalDateTime.now(), "User Conflict", ex.getMessage()); return ResponseEntity.status(HttpStatus.CONFLICT).body(errorResponse); } @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<ErrorResponseDto> handleResourceNotFound(ResourceNotFoundException ex){ ErrorResponseDto errorResponse = new ErrorResponseDto(HttpStatus.NOT_FOUND.value(), LocalDateTime.now(), "Resource Not Found", ex.getMessage()); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); } @ExceptionHandler(Exception.class) public ResponseEntity<ErrorResponseDto> handleGeneric(Exception ex) { ErrorResponseDto errorResponse = new ErrorResponseDto( HttpStatus.INTERNAL_SERVER_ERROR.value(), LocalDateTime.now(), "Internal Server Error", ex.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); } }
Spring Boot Application Class
Application class with the main method which is the entry point for Spring Boot application.
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class UserProjApplication { public static void main(String[] args) { SpringApplication.run(UserProjApplication.class, args); } }
Testing the application
Run the REST application by running the SpringdataDemoApplication.java class.
For testing the application we'll use the rest client Postman, ensure that you have Postman app installed.
Inserting User record.
- Select POST as request type.
- URL as http://localhost:8080/user/
- Select Body and provide user data
{ "firstName": "Kavin", "lastName": "Michael", "userType": "Silver", "startDate": "2018-08-20" }
- Specify content type as JSON.
- Click Send
In the response body you should see the status as “201 Created” and also the created user record. Note that the Id is also added in the returned record.
Getting specific user record
- Select GET as request type.
- URL as http://localhost:8080/user/5 (5 is userId)
- Click Send
Same way you can get all the users by Selecting GET as request type and URL as http://localhost:8080/user
Updating user record
- Select PUT as request type.
- URL as http://localhost:8080/user
- Select Body and provide data with UserId for the user that has to be updated.
{ "userId": 6, "firstName": "Kavin", "lastName": "Michael", "userType": "Gold", "startDate": "2018-08-20" }
- Click Send
In the returned data you can verify that the user_type is modified for user id 6. Same can also be verified in the database.
Deleting user record
- Select Delete as request type.
- URL as http://localhost:8080/user/45 to delete User with Id as 45.
- Click Send
If you try to delete by giving an id that doesn't exist then you should get response status as "404 Not Found" and response body as created ErrorResponseDto.
Getting user by user type
- Select Get as request type.
- URL http://localhost:8080/user/type?type=platinum to get all users having user type as platinum.
- Click Send
When validation error occurs
- Select POST as request type.
- URL as http://localhost:8080/user
- Select Body and provide user data
{ "firstName": "", "lastName": "Michael", "userType": "Silver", "startDate": "2025-08-20" }
- Specify content type as JSON.
- Click Send
That's all for this topic Spring Boot + Data JPA + MySQL REST API CRUD Example. If you have any doubt or any suggestions to make please drop a comment. Thanks!
>>>Return to Spring Tutorial Page
Related Topics
You may also like-
Here you have accessed service from controller. But the flow should be controller-repository-service isn't it. Please explain.
ReplyDeleteNo, it's the other way round Controller-Service-Repository(DAO layer).. That way you keep your web layer (Controller) separate from DAO Layer. Service class is supposed to be a stand alone class in between having the business logic.
Delete