How to create RESTful API with Spring Boot 2.1,
Introduction
How to create a REST API with Spring Boot 2.1, Spring Data JPA and Hibernate
Introduction
In this guide, we will show you how to build simple RESTful web services from scratch using Spring Boot 2.1, Spring Data JPA/Hibernate with MySQL as database.
Requirements
- JDK 8
- Maven 3.x
- MySQL 5.7
Main Technologies
- SpringBoot 2.1.2
- Spring Data JPA 2.1.4
- Hibernate 5.3.7
Maven dependencies
[embed]
Three Layers of Web Service

Presentation/Controller Layer
It is top most layer and it receives input/request from client and invokes service layer. The result from service layer is processed and response is sent back to client.
Service Layer
It is middle layer and it called by Controller layer. It does all business operation and calls lowermost repository/data layer.
Persistence /Data/Repository Layer
It does all interaction with underlying database.
Order Management Service We are going to build a simple order management RESTful service which will represent CRUD operations for two entities User and Order. We will be using above mentioned three layers for this application: Presentation, Service, and Persistence.
Domain Model
Our domain model would consists of User and Order as shown below.
public class User {
private Long id;
private String firstName;
private String lastName;
private String email;
private String firstLineOfAddress;
private String secondLineOfAddress;
private String town;
private String postCode;
// Getters and Setters (Omitted for brevity)
}
public class Order {
private Long id;
private String description;
private long priceInPence;
private boolean completedStatus = false;
// Getters and Setters (Omitted for brevity)
}
Service Layer- Interface Lets implement service layer by “programming to interface” principle
public interface UserService {
User createUser(User user);
User updateUser(Long id, User user);
User patchUpdateUser(Long id, User user);
User getUserById(Long id);
List<User> getUsers();
void deleteUser(Long id);
}
public interface OrderService {
Order createOrder(Long userId, Order order);
Order updateOrder(Long userId, Long orderId, Order order);
Order patchUpdateOrder(Long userId, Long orderId, Order order);
Order getOrder(Long userId, Long orderId);
List<Order> getAllOrdersByUserId(Long userId);
void deleteOrder(Long userId, Long orderId);
}
Both UserService and OrderService represents CRUD operations on user and order respectively. PatchUpdate refers to partial update where all the fields are not updated at the same time, where as update refers to update where all the fields are used for update. In order to implement above interfaces, we need to have persistence layer which will actually stores/retrieves data from database. Lets revisit service layer once persistence layer is implemented.
Persistence Layer
Implementing persistence/data layer in Spring is very simple and is two step process.
- Convert domain into entity object.
@Entity(name = "users")
public class User implements Serializable {
private static final long serialVersionUID = -465L;
@Id
@GeneratedValue
private Long id;
@Column(nullable = false, length = 50)
private String firstName;
@Column(nullable = false, length = 50)
private String lastName;
@Column(nullable = false, length = 120)
private String email;
@Column(nullable = false, length = 50)
private String firstLineOfAddress;
@Column(length = 50)
private String secondLineOfAddress;
@Column(nullable = false, length = 50)
private String town;
@Column(nullable = false, length = 10)
private String postCode;
// Getters and Setters (Omitted for brevity)
}
Above annotations are basically JPA annotations.
**@Entity(name = “users”) tells that data contained in class will be stored in database table “users”
`@Column(nullable = false, length = 50)** Indicates column name used in database table. Length indicates permitted column length in table. Nullable indicates if the column in table can be null or not. Because every user may not have second line of address, we made this field nullable. Domain objects are sometimes stored in http-session for caching/optimisation purpose, hence making the objectSerializable` is a good practice.
**serialVersionUID **is usually very big long number.
Similarly Order Entity is shown below.
@Entity(name = "orders")
public class Order implements Serializable {
private static final long serialVersionUID = -460L;
@Id
@GeneratedValue
private Long id;
@Column(nullable = false, length = 120)
private String description;
@Column(nullable = false, length = 120)
private long priceInPence;
@Column(nullable = false)
private boolean completedStatus = false;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
@JsonIgnore
private User user;
// Getters and Setters (Omitted for brevity)
}
**@ManyToOne(fetch = FetchType.LAZY, optional = false) indicates User to Order is many to one mapping, which means a user can have zero
or many orders. But one order can belong to one user only.
`@JoinColumn(name = “user_id”, nullable = false)** indicates Order table will have user_id column which will reference id of users table. @OnDelete(action = OnDeleteAction.CASCADE)` indicates that parent user row is deleted in users table, corresponding order row will also
be deleted in the orders table.
**@JsonIgnore** is used to hide the field, when order is serialised into json response body , which is sent to client. We don’t want user details in
order response body.
- Create Repository interface which will extend spring interface CrudRepository.
User Repository
@Repository
public interface UserRepository extends CrudRepository<User, Long> {
Optional<User> findByEmail(String email);
}
Spring will automatically implement above interface.
**@Repository** is Spring annotation to indicate that it is data layer.
**findByEmail** finds User by its email field.
Order Repository
@Repository
public interface OrderRepository extends CrudRepository<Order, Long> {
Optional<Order> findByIdAndUserId(Long id, Long userId);
List<Order> findAllByUser(User user);
}
**findByIdAndUserId **returns order by OrderId and UserId.
**findAllByUser** returns order by user.
This completes our persistence/data/repository layer implementation and now can move back to service layer.
Service Layer- Implementation
UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
public User getUserById(Long id) {
Optional<User> maybeUser = userRepository.findById(id);
return maybeUser.orElseThrow(() ->
new RuntimeException("User not found"));
}
@Override
public User createUser(User user) {
userRepository.findByEmail(user.getEmail())
.ifPresent(p -> {
throw new RuntimeException("User with email "
+ p.getEmail() + " already exists ");
});
return userRepository.save(user);
}
@Override
public User updateUser(Long id, User user) {
User userFound = userRepository.findById(id)
.orElseThrow(() ->
new RuntimeException("User not found"));
userFound.setFirstName(user.getFirstName());
userFound.setLastName(user.getLastName());
userFound.setEmail(user.getEmail());
userFound.setFirstLineOfAddress(
user.getFirstLineOfAddress());
userFound.setSecondLineOfAddress(
user.getSecondLineOfAddress());
userFound.setTown(user.getTown());
userFound.setPostCode(user.getPostCode());
return userRepository.save(userFound);
}
@Override
public User patchUpdateUser(Long id, User user) {
User userFound = userRepository.findById(id)
.orElseThrow(() ->
new RuntimeException("User not found"));
ModelMapper modelMapper = new ModelMapper();
//copy only non null values
modelMapper.getConfiguration().setSkipNullEnabled(true)
.setMatchingStrategy(MatchingStrategies.STRICT);
modelMapper.map(user, userFound);
return userRepository.save(userFound);
}
@Override
public void deleteUser(Long id) {
User userFound = userRepository.findById(id)
.orElseThrow(() ->
new RuntimeException("User not found"));
userRepository.delete(userFound);
}
@Override
public List<User> getUsers() {
Iterable<User> userIterable = userRepository.findAll();
return StreamSupport
.stream(userIterable.spliterator(), false)
.collect(Collectors.toList());
}
}
**@Service**tells spring that it is service layer and will be injected where ever corresponding @Autowired is found.
**@Autowired **in useRepository indicates that Spring will automatically create instance of UserRepository and inject into the concerned class.
**patchUpdateUser(Long id, User user) When patchUpdateUser is called from presentation/Controller layer, User object fields might
automatically be mapped to null, for the fields which are not part of input request. We don’t want to copy null values when `modelMapper.map(
user, userFound)`** is invoked, hence to skip copying null value.
**modelMapper.getConfiguration().setSkipNullEnabled(true).setMatchingStrategy(MatchingStrategies.STRICT) **is used.
**modelMapper.map(user, userFound) **is used to copy fields from user object to userFound object.
Rest of code in Service layer is self explanatory.
OrderServiceImpl
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private UserRepository userRepository;
@Override
public Order createOrder(Long userId, Order order) {
User user = userRepository.findById(userId)
.orElseThrow(() ->
new RuntimeException("User with id "
+ userId + " Not found"));
order.setUser(user);
return orderRepository.save(order);
}
@Override
public Order getOrder(Long userId, Long orderId) {
return orderRepository.findByIdAndUserId(orderId, userId)
.orElseThrow(() ->
new RuntimeException("User with id "
+ userId + " and order id "
+ orderId + " Not found"));
}
@Override
public List<Order> getAllOrdersByUserId(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() ->
new RuntimeException("User with id "
+ userId + " Not found"));
return orderRepository.findAllByUser(user);
}
@Override
public Order updateOrder(Long userId,
Long orderId, Order order) {
Order orderFound = orderRepository
.findByIdAndUserId(orderId, userId)
.orElseThrow(() ->
new RuntimeException("User with id "
+ userId + " and order id "
+ orderId + " Not found"));
orderFound.setDescription(order.getDescription());
orderFound.setPriceInPence(order.getPriceInPence());
return orderRepository.save(orderFound);
}
@Override
public Order patchUpdateOrder(Long userId,
Long orderId, Order order) {
Order orderFound = orderRepository
.findByIdAndUserId(orderId, userId)
.orElseThrow(() ->
new RuntimeException("User with id "
+ userId + " and order id "
+ orderId + " Not found"));
if (order.getDescription() != null)
orderFound.setDescription(order.getDescription());
if (order.getPriceInPence() != 0)
orderFound.setPriceInPence(order.getPriceInPence());
return orderRepository.save(orderFound);
}
@Override
public void deleteOrder(Long userId, Long orderId) {
Order order = orderRepository
.findByIdAndUserId(orderId, userId)
.orElseThrow(() ->
new RuntimeException("User with id "
+ userId + " and order id "
+ orderId + " Not found"));
orderRepository.delete(order);
}
}
OrderServiceImpl is very similar to UserServiceImpl and is self explanatory.
Let’s now move to Presentation Layer.
Presentation Layer
Presentation Layer, also called Controller layer is topmost layer and it interacts with user. Its is responsible to receive request from client and sends back response.
UserController
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
@GetMapping()
public List<User> getAllUsers() {
return userService.getUsers();
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User createdUser = userService.createUser(user);
return ResponseEntity
.status(HttpStatus.CREATED).body(createdUser);
}
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id,
@RequestBody User user) {
return userService.updateUser(id, user);
}
@PatchMapping("/{id}")
public User patchUpdateUser(@PathVariable Long id,
@RequestBody User user) {
return userService.patchUpdateUser(id, user);
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
}
**@RestController tells Spring that is is Rest controller class.
`@RequestMapping(“/api/users”)**prefix for Url of request that will be mapped to controller class. @GetMapping(“/{id}”)` indicates that concerned method will invoked when GET request is received from the client. **{id}indicates id of the resource . So the Url of request mapped to concerned method will be /api/users/{id}** **@PostMapping @PutMapping @PatchMapping @DeleteMapping represents Http methods Post, Put, Patch and Delete respectively.
`ResponseEntity<User> createUser(@RequestBody User user) **When Http Post request is received, spring automatically converts json request payload into User object. If any fields are missing in request payload, the fields in User object will be mapped to null ResponseEntity<User>` returned by createUser will contain both http status code(201 for POST) and response body. Spring framework automatically converts into json response body along with status. Wherever ResponseEntity is not returned , default status code of 200 is returned e.g getUser will returns default status code of 200 and along with user as response body.
You might have noted that , code in Controller Class is very clean and small as most of heavy lifting is done by lower service layer.
OrderController
@RestController
@RequestMapping("/api/users")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/{userId}/orders")
public ResponseEntity<Order> createOrder(
@PathVariable Long userId,
@RequestBody Order order) {
Order createdOrder = orderService.createOrder(userId,order);
return ResponseEntity
.status(HttpStatus.CREATED).body(createdOrder);
}
@GetMapping("/{userId}/orders/{orderId}")
public Order getOrder(@PathVariable Long userId,
@PathVariable Long orderId) {
return orderService.getOrder(userId, orderId);
}
@PutMapping("/{userId}/orders/{orderId}")
public Order updateOrder(@PathVariable Long userId,
@PathVariable Long orderId,
@RequestBody Order order) {
return orderService.updateOrder(userId, orderId, order);
}
@PatchMapping("/{userId}/orders/{orderId}")
public Order patchUpdateOrder(@PathVariable Long userId,
@PathVariable Long orderId,
@RequestBody Order order) {
return orderService.patchUpdateOrder(userId, orderId,order);
}
@GetMapping("/{userId}/orders")
public List<Order> getAllOrders(@PathVariable Long userId) {
return orderService.getAllOrdersByUserId(userId);
}
@DeleteMapping("/{userId}/orders/{orderId}")
public ResponseEntity<?> deleteOrder(@PathVariable Long userId,
@PathVariable Long orderId) {
orderService.deleteOrder(userId, orderId);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
}
OrderController is similar to UserController.
Test services manually We can test services manually with Postman . Following shows request and response for various operations.
Post User
URL http://localhost:8080/api/users
RequestBody
{
"firstName":"Steve",
"lastName":"Rob",
"email":"steverob@test.com",
"firstLineOfAddress":"111 Pinewood Grove",
"secondLineOfAddress":"Commercial street",
"town":"London",
"postCode":"W7 8AG"
}
Response
{
"id": 44,
"firstName": "Steve",
"lastName": "Rob",
"email": "steverob@test.com",
"firstLineOfAddress": "111 Pinewood Grove",
"secondLineOfAddress": "Commercial street",
"town": "London",
"postCode": "W7 8AG"
}
Get User
URL http://localhost:8080/api/users/44 Response
{
"id": 44,
"firstName": "Steve",
"lastName": "Rob",
"email": "steverob@test.com",
"firstLineOfAddress": "111 Pinewood Grove",
"secondLineOfAddress": "Commercial street",
"town": "London",
"postCode": "W7 8AG"
}
Put User
URL http://localhost:8080/api/users/44
RequestBody
{
"firstName":"Steveupdated",
"lastName":"Robupdated",
"email":"steverob@test.com",
"firstLineOfAddress":"111 Pinewood Grove",
"secondLineOfAddress":"Commercial street",
"town":"London",
"postCode":"W7 8AG"
}
Response
{
"id": 44,
"firstName": "Steveupdated",
"lastName": "Robupdated",
"email": "steverob@test.com",
"firstLineOfAddress": "111 Pinewood Grove",
"secondLineOfAddress": "Commercial street",
"town": "London",
"postCode": "W7 8AG"
}
Patch User
URL http://localhost:8080/api/users/44
RequestBody
{
"firstName":"SteveupdatedAgain",
"lastName":"RobupdatedAgain"
}
Response
{
"id": 44,
"firstName": "SteveupdatedAgain",
"lastName": "RobupdatedAgain",
"email": "steverob@test.com",
"firstLineOfAddress": "111 Pinewood Grove",
"secondLineOfAddress": "Commercial street",
"town": "London",
"postCode": "W7 8AG"
}
Post Order
URL http://localhost:8080/api/api/users/44/orders
RequestBody
{
"description":"Awesome phone",
"priceInPence":1200,
"completedStatus": false
}
Response
{
"id": 45,
"description":"Awesome phone",
"priceInPence":1200,
"completedStatus": false
}
Get Order
URL http://localhost:8080/api/users/44/orders/45
Response
{
"id": 45,
"description":"Awesome phone",
"priceInPence":1200,
"completedStatus": false
}
Complete Source Code Location
메타데이터
- post_id
- fee9f477e8a7
- slug
- how-to-create-restful-api-with-spring-boot-2-1-fee9f477e8a7
- url
- https://medium.com/ranjeshblogs/how-to-create-restful-api-with-spring-boot-2-1-fee9f477e8a7
- canonical_url
- https://medium.com/ranjeshblogs/how-to-create-restful-api-with-spring-boot-2-1-fee9f477e8a7
- author_url
- https://medium.com/@ranjesh1
- status
- ok
- fetched_at
- 2026-07-29 18:51:22