- Introduction
- REST Basics
- JSON Basics
- HTTP Basics
- Spring REST Controller
- JSON Data Binding with Jackson
- Exception Handling
- REST API Design
- Real-Time Project Architecture
- Spring Data JPA
- Spring Data REST
- API Documentation with OpenAPI/Swagger
This guide covers building REST APIs and Web Services with Spring Boot 3, including:
- Creating REST APIs with Spring
- Understanding REST concepts, JSON, and HTTP messaging
- Using Postman for testing
- Developing REST APIs with
@RestController - Building CRUD interfaces with Spring REST
REST = REpresentational State Transfer
- Lightweight approach for communicating between applications
- Makes REST API calls over HTTP
- Language independent (Java, JavaScript, Python, C#, Go, PHP, Ruby, Swift, etc.)
- Can use any data format (commonly JSON or XML)
┌─────────────────┐ ┌─────────────────┐
│ Weather App │────────>│ Weather Service │
│ (CLIENT) │<────────│ (SERVER) │
│ │ REST │ (External) │
└─────────────────┘ API └─────────────────┘
All of these terms generally mean the SAME thing:
- REST API
- RESTful API
- REST Web Services
- RESTful Web Services
- REST Services
- RESTful Services
JSON = JavaScript Object Notation
- Lightweight data format for storing and exchanging data (plain text)
- Language independent
- Can be used with any programming language
{
"id": 14,
"firstName": "Mario",
"lastName": "Rossi",
"active": true
}Rules:
- Curly braces
{}define objects - Object members are name/value pairs
- Delimited by colons
: - Name is always in double quotes
{
"id": 14, // Number: no quotes
"firstName": "Mario", // String: in double quotes
"lastName": "Rossi",
"active": true, // Boolean: true/false
"courses": null // null value
}{
"id": 14,
"firstName": "Mario",
"lastName": "Rossi",
"active": true,
"address": {
"street": "100 Main St",
"city": "Philadelphia",
"state": "Pennsylvania",
"zip": "19103",
"country": "USA"
}
}{
"id": 14,
"firstName": "Mario",
"lastName": "Rossi",
"active": true,
"languages": ["Java", "C#", "Python", "Javascript"]
}| HTTP Method | CRUD Operation |
|---|---|
| POST | Create a new entity |
| GET | Read a list of entities or single entity |
| PUT | Update an existing entity |
| PATCH | Partial update of an existing entity |
| DELETE | Delete an existing entity |
┌──────────────┐ ┌──────────────┐
│ │ HTTP Request │ │
│ CRM Client │───────────────────>│ CRM REST │
│ App │ │ Service │
│ │ HTTP Response │ (Server) │
│ │<───────────────────│ │
└──────────────┘ └──────────────┘
Request line: GET /api/employees HTTP/1.1
Header variables:
Host: localhost:8080
Content-Type: application/json
Message body: [request payload]
Response line: HTTP/1.1 200 OK
Header variables:
Content-Type: application/json
Content-Length: 1234
Message body: [response payload]
| Code Range | Description |
|---|---|
| 100-199 | Informational |
| 200-299 | Successful |
| 300-399 | Redirection |
| 400-499 | Client error |
| 500-599 | Server error |
Common Status Codes:
200 OK- Request succeeded201 Created- Resource created401 Unauthorized- Authentication required404 Not Found- Resource not found500 Internal Server Error- Server error
Syntax: type/sub-type
Examples:
text/htmltext/plainapplication/jsonapplication/xml
@RestController
@RequestMapping("/test")
public class DemoRestController {
@GetMapping("/hello")
public String sayHello() {
return "Hello World!";
}
}Access at: http://localhost:8080/test/hello
Step 1: Add Maven Dependency
<!-- Add Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>Step 2: Create REST Controller
@RestController
@RequestMapping("/test")
public class DemoRestController {
@GetMapping("/hello")
public String sayHello() {
return "Hello World!";
}
}Data binding is the process of converting JSON data to a Java POJO and vice versa.
JSON <──────────> Java POJO
Data Binding
Also known as:
- Mapping
- Serialization / Deserialization
- Marshalling / Unmarshalling
Spring uses the Jackson Project behind the scenes for JSON data binding.
GitHub: https://github.com/FasterXML/jackson-databind
{
"id": 14,
"firstName": "Mario",
"lastName": "Rossi",
"active": true
}public class Student {
private int id;
private String firstName;
private String lastName;
private boolean active;
// Jackson calls setter methods
public void setId(int id) {
this.id = id;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setActive(boolean active) {
this.active = active;
}
// getter methods...
}Note: Jackson calls the setXXX() methods. It does NOT access internal private fields directly.
Jackson calls getter methods on POJO to convert to JSON.
When building Spring REST applications:
- Spring automatically handles Jackson integration
- JSON data passed to REST controller is converted to POJO
- Java object returned from REST controller is converted to JSON
This happens automatically behind the scenes!
File: src/main/java/com/example/entity/Employee.java
package com.example.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Email;
@Entity
@Table(name = "employees")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@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;
@Email(message = "Email must be valid")
@Column(name = "email", unique = true)
private String email;
@Column(name = "department")
private String department;
@Column(name = "salary")
private Double salary;
// Constructors
public Employee() {
}
public Employee(String firstName, String lastName, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public Employee(String firstName, String lastName, String email,
String department, Double salary) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.department = department;
this.salary = salary;
}
// Getters and Setters
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
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 getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getDepartment() {
return department;
}
public void setDepartment(String department) {
this.department = department;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", department='" + department + '\'' +
", salary=" + salary +
'}';
}
}File: src/main/java/com/example/repository/EmployeeRepository.java
package com.example.repository;
import com.example.entity.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
List<Employee> findByDepartment(String department);
List<Employee> findByFirstName(String firstName);
@Query("SELECT e FROM Employee e WHERE e.salary > :minSalary ORDER BY e.salary DESC")
List<Employee> findEmployeesWithSalaryGreaterThan(@Param("minSalary") Double minSalary);
}File: src/main/java/com/example/service/EmployeeService.java
package com.example.service;
import com.example.entity.Employee;
import com.example.repository.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
public class EmployeeService {
@Autowired
private EmployeeRepository employeeRepository;
public List<Employee> getAllEmployees() {
return employeeRepository.findAll();
}
public Employee getEmployeeById(Integer id) {
Optional<Employee> employee = employeeRepository.findById(id);
return employee.orElseThrow(() ->
new RuntimeException("Employee not found with id: " + id));
}
public List<Employee> getEmployeesByDepartment(String department) {
return employeeRepository.findByDepartment(department);
}
public List<Employee> getHighEarners(Double minSalary) {
return employeeRepository.findEmployeesWithSalaryGreaterThan(minSalary);
}
@Transactional
public Employee createEmployee(Employee employee) {
// Check if email already exists
return employeeRepository.save(employee);
}
@Transactional
public Employee updateEmployee(Integer id, Employee employeeDetails) {
Employee employee = getEmployeeById(id);
employee.setFirstName(employeeDetails.getFirstName());
employee.setLastName(employeeDetails.getLastName());
employee.setEmail(employeeDetails.getEmail());
employee.setDepartment(employeeDetails.getDepartment());
employee.setSalary(employeeDetails.getSalary());
return employeeRepository.save(employee);
}
@Transactional
public void deleteEmployee(Integer id) {
Employee employee = getEmployeeById(id);
employeeRepository.delete(employee);
}
}File: src/main/java/com/example/controller/EmployeeController.java
package com.example.controller;
import com.example.entity.Employee;
import com.example.service.EmployeeService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/employees")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
// GET all employees
@GetMapping
public ResponseEntity<List<Employee>> getAllEmployees() {
List<Employee> employees = employeeService.getAllEmployees();
return ResponseEntity.ok(employees);
}
// GET employee by ID
@GetMapping("/{id}")
public ResponseEntity<Employee> getEmployeeById(@PathVariable Integer id) {
try {
Employee employee = employeeService.getEmployeeById(id);
return ResponseEntity.ok(employee);
} catch (RuntimeException e) {
return ResponseEntity.notFound().build();
}
}
// GET employees by department
@GetMapping("/department/{dept}")
public ResponseEntity<List<Employee>> getByDepartment(
@PathVariable String dept) {
List<Employee> employees = employeeService.getEmployeesByDepartment(dept);
return ResponseEntity.ok(employees);
}
// POST - Create new employee
@PostMapping
public ResponseEntity<Employee> createEmployee(
@Valid @RequestBody Employee employee) {
Employee createdEmployee = employeeService.createEmployee(employee);
return ResponseEntity.status(HttpStatus.CREATED).body(createdEmployee);
}
// PUT - Update employee
@PutMapping("/{id}")
public ResponseEntity<Employee> updateEmployee(
@PathVariable Integer id,
@Valid @RequestBody Employee employeeDetails) {
try {
Employee updatedEmployee = employeeService.updateEmployee(id, employeeDetails);
return ResponseEntity.ok(updatedEmployee);
} catch (RuntimeException e) {
return ResponseEntity.notFound().build();
}
}
// DELETE employee
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteEmployee(@PathVariable Integer id) {
try {
employeeService.deleteEmployee(id);
return ResponseEntity.noContent().build();
} catch (RuntimeException e) {
return ResponseEntity.notFound().build();
}
}
}File: src/main/java/com/example/exception/GlobalExceptionHandler.java
package com.example.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.MethodArgumentNotValidException;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException ex) {
ErrorResponse errorResponse = new ErrorResponse(
"Validation failed",
ex.getBindingResult().getFieldError().getDefaultMessage(),
400
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorResponse> handleRuntimeException(
RuntimeException ex) {
ErrorResponse errorResponse = new ErrorResponse(
"Error",
ex.getMessage(),
500
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(errorResponse);
}
}File: src/main/java/com/example/exception/ErrorResponse.java
package com.example.exception;
public class ErrorResponse {
private String error;
private String message;
private int status;
public ErrorResponse(String error, String message, int status) {
this.error = error;
this.message = message;
this.status = status;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
}# GET all employees
curl http://localhost:8080/api/v1/employees
# GET employee by ID
curl http://localhost:8080/api/v1/employees/1
# GET employees by department
curl http://localhost:8080/api/v1/employees/department/Engineering
# POST - Create new employee
curl -X POST http://localhost:8080/api/v1/employees \
-H "Content-Type: application/json" \
-d '{
"firstName":"John",
"lastName":"Doe",
"email":"john@example.com",
"department":"Engineering",
"salary":75000
}'
# PUT - Update employee
curl -X PUT http://localhost:8080/api/v1/employees/1 \
-H "Content-Type: application/json" \
-d '{
"firstName":"Jane",
"lastName":"Doe",
"email":"jane@example.com",
"department":"Marketing",
"salary":65000
}'
# DELETE employee
curl -X DELETE http://localhost:8080/api/v1/employees/1File: src/test/java/com/example/controller/EmployeeControllerTest.java
package com.example.controller;
import com.example.entity.Employee;
import com.example.service.EmployeeService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
public class EmployeeControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private EmployeeService employeeService;
@Test
public void testGetAllEmployees() throws Exception {
Employee emp1 = new Employee("John", "Doe", "john@example.com", "IT", 70000.0);
Employee emp2 = new Employee("Jane", "Smith", "jane@example.com", "HR", 65000.0);
when(employeeService.getAllEmployees())
.thenReturn(Arrays.asList(emp1, emp2));
mockMvc.perform(get("/api/v1/employees"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(2))
.andExpect(jsonPath("$[0].firstName").value("John"));
}
@Test
public void testCreateEmployee() throws Exception {
Employee employee = new Employee("Bob", "Wilson", "bob@example.com", "Finance", 60000.0);
Employee savedEmployee = new Employee("Bob", "Wilson", "bob@example.com", "Finance", 60000.0);
savedEmployee.setId(1);
when(employeeService.createEmployee(any(Employee.class)))
.thenReturn(savedEmployee);
mockMvc.perform(post("/api/v1/employees")
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().writeValueAsString(employee)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.firstName").value("Bob"));
}
}}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
### REST Controller - Return List of Students
```java
@RestController
@RequestMapping("/api")
public class StudentRestController {
// Endpoint: GET /api/students
@GetMapping("/students")
public List<Student> getStudents() {
List<Student> theStudents = new ArrayList<>();
theStudents.add(new Student("Poornima", "Patel"));
theStudents.add(new Student("Mario", "Rossi"));
theStudents.add(new Student("Mary", "Smith"));
return theStudents; // Jackson converts to JSON array
}
}
@RestController
@RequestMapping("/api")
public class StudentRestController {
// Endpoint: GET /api/students/{studentId}
@GetMapping("/students/{studentId}")
public Student getStudent(@PathVariable int studentId) {
List<Student> theStudents = new ArrayList<>();
// populate theStudents...
// Return student at index
return theStudents.get(studentId);
}
}Example Requests:
/api/students/0→ First student/api/students/1→ Second student/api/students/2→ Third student
When requesting an invalid student ID (e.g., 9999), the application returns an ugly error page.
┌──────────────┐ ┌──────────────┐
│ │ /api/students/ │ │
│ REST Client │ 9999 │ REST Service│
│ │───────────────────>│ │
│ │ │ Throw │
│ │ │ Exception │
│ │ └──────┬───────┘
│ │ │
│ │ v
│ │ ┌──────────────┐
│ │ Error Response │ Exception │
│ │<───────────────────│ Handler │
└──────────────┘ └──────────────┘
Step 1: Create Custom Error Response Class
public class StudentErrorResponse {
private int status;
private String message;
private long timeStamp;
// Constructors
public StudentErrorResponse() {
}
public StudentErrorResponse(int status, String message, long timeStamp) {
this.status = status;
this.message = message;
this.timeStamp = timeStamp;
}
// Getters and Setters
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public long getTimeStamp() {
return timeStamp;
}
public void setTimeStamp(long timeStamp) {
this.timeStamp = timeStamp;
}
}Step 2: Create Custom Exception
public class StudentNotFoundException extends RuntimeException {
public StudentNotFoundException(String message) {
super(message);
}
}Step 3: Update REST Service to Throw Exception
@RestController
@RequestMapping("/api")
public class StudentRestController {
@GetMapping("/students/{studentId}")
public Student getStudent(@PathVariable int studentId) {
List<Student> theStudents = new ArrayList<>();
// populate theStudents...
// Check if studentId is valid
if ((studentId >= theStudents.size()) || (studentId < 0)) {
throw new StudentNotFoundException("Student id not found - " + studentId);
}
return theStudents.get(studentId);
}
}Step 4: Add Exception Handler Method
@RestController
@RequestMapping("/api")
public class StudentRestController {
@ExceptionHandler
public ResponseEntity<StudentErrorResponse> handleException(
StudentNotFoundException exc) {
StudentErrorResponse error = new StudentErrorResponse();
error.setStatus(HttpStatus.NOT_FOUND.value());
error.setMessage(exc.getMessage());
error.setTimeStamp(System.currentTimeMillis());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
}Problem: Exception handler code is only for specific REST controller. Cannot be reused.
Solution: Use @ControllerAdvice for global exception handling.
@ControllerAdvice
public class StudentRestExceptionHandler {
@ExceptionHandler
public ResponseEntity<StudentErrorResponse> handleException(
StudentNotFoundException exc) {
StudentErrorResponse error = new StudentErrorResponse();
error.setStatus(HttpStatus.NOT_FOUND.value());
error.setMessage(exc.getMessage());
error.setTimeStamp(System.currentTimeMillis());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
// Add more exception handlers as needed...
}Benefits:
- Promotes reuse
- Centralizes exception handling
- Works across multiple controllers
- Review API requirements
- Identify main resource/entity
- Use HTTP methods to assign actions
Requirements:
- Get a list of employees
- Get a single employee by id
- Add a new employee
- Update an employee
- Delete an employee
Main Resource: employee → Use plural: /api/employees
| HTTP Method | Endpoint | CRUD Action |
|---|---|---|
| POST | /api/employees |
Create a new employee |
| GET | /api/employees |
Read a list of employees |
| GET | /api/employees/{employeeId} |
Read a single employee |
| PUT | /api/employees |
Update an existing employee |
| PATCH | /api/employees/{employeeId} |
Partial update of employee |
| DELETE | /api/employees/{employeeId} |
Delete an existing employee |
DON'T DO THIS:
❌ /api/employeesList
❌ /api/deleteEmployee
❌ /api/addEmployee
❌ /api/updateEmployee
Instead, use HTTP methods to assign actions!
PayPal Invoicing API:
GitHub Repositories API:
Salesforce REST API:
┌─────────────────────────┐
│ Employee REST │
│ Controller │ ← Handles HTTP requests/responses
└───────────┬─────────────┘
│
v
┌─────────────────────────┐
│ Employee Service │ ← Business logic layer
└───────────┬─────────────┘
│
v
┌─────────────────────────┐
│ Employee DAO │ ← Data access layer
└───────────┬─────────────┘
│
v
[Database]
File: employee.sql
CREATE TABLE employee (
id INT PRIMARY KEY AUTO_INCREMENT,
first_name VARCHAR(45),
last_name VARCHAR(45),
email VARCHAR(45)
);
INSERT INTO employee VALUES
(1, 'Leslie', 'Andrews', 'leslie@myapp.com'),
(2, 'Emma', 'Baumgarten', 'emma@myapp.com'),
(3, 'Avani', 'Gupta', 'avani@myapp.com');@Entity
@Table(name="employee")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="id")
private int id;
@Column(name="first_name")
private String firstName;
@Column(name="last_name")
private String lastName;
@Column(name="email")
private String email;
// Constructors, getters, setters...
}public interface EmployeeDAO {
List<Employee> findAll();
Employee findById(int theId);
Employee save(Employee theEmployee);
void deleteById(int theId);
}@Repository
public class EmployeeDAOJpaImpl implements EmployeeDAO {
private EntityManager entityManager;
@Autowired
public EmployeeDAOJpaImpl(EntityManager theEntityManager) {
entityManager = theEntityManager;
}
@Override
public List<Employee> findAll() {
TypedQuery<Employee> theQuery =
entityManager.createQuery("from Employee", Employee.class);
List<Employee> employees = theQuery.getResultList();
return employees;
}
@Override
public Employee findById(int theId) {
Employee theEmployee = entityManager.find(Employee.class, theId);
return theEmployee;
}
@Override
public Employee save(Employee theEmployee) {
// If id == 0, insert; otherwise update
Employee dbEmployee = entityManager.merge(theEmployee);
return dbEmployee; // Returns updated id
}
@Override
public void deleteById(int theId) {
Employee theEmployee = entityManager.find(Employee.class, theId);
entityManager.remove(theEmployee);
}
}- Service Facade design pattern
- Intermediate layer for custom business logic
- Integrate data from multiple sources (DAOs/repositories)
- Apply transactional boundaries
┌─────────────────────────┐
│ Employee REST │
│ Controller │
└───────────┬─────────────┘
│
v
┌─────────────────────────┐
│ Employee Service │ ← Business logic + Transactions
└─────┬───────────┬───────┘
│ │
v v
┌──────────┐ ┌──────────┐
│Employee │ │ Skills │
│ DAO │ │ DAO │
└──────────┘ └──────────┘
public interface EmployeeService {
List<Employee> findAll();
Employee findById(int theId);
Employee save(Employee theEmployee);
void deleteById(int theId);
}@Service
public class EmployeeServiceImpl implements EmployeeService {
private EmployeeDAO employeeDAO;
@Autowired
public EmployeeServiceImpl(EmployeeDAO theEmployeeDAO) {
employeeDAO = theEmployeeDAO;
}
@Override
@Transactional
public List<Employee> findAll() {
return employeeDAO.findAll();
}
@Override
@Transactional
public Employee findById(int theId) {
return employeeDAO.findById(theId);
}
@Override
@Transactional
public Employee save(Employee theEmployee) {
return employeeDAO.save(theEmployee);
}
@Override
@Transactional
public void deleteById(int theId) {
employeeDAO.deleteById(theId);
}
}Note: Apply @Transactional at service layer, not DAO layer.
@RestController
@RequestMapping("/api")
public class EmployeeRestController {
private EmployeeService employeeService;
@Autowired
public EmployeeRestController(EmployeeService theEmployeeService) {
employeeService = theEmployeeService;
}
// GET all employees
@GetMapping("/employees")
public List<Employee> findAll() {
return employeeService.findAll();
}
// GET single employee
@GetMapping("/employees/{employeeId}")
public Employee getEmployee(@PathVariable int employeeId) {
Employee theEmployee = employeeService.findById(employeeId);
if (theEmployee == null) {
throw new RuntimeException("Employee id not found - " + employeeId);
}
return theEmployee;
}
// POST - create new employee
@PostMapping("/employees")
public Employee addEmployee(@RequestBody Employee theEmployee) {
// Force id to 0 for new insert
theEmployee.setId(0);
Employee dbEmployee = employeeService.save(theEmployee);
return dbEmployee;
}
// PUT - update existing employee
@PutMapping("/employees")
public Employee updateEmployee(@RequestBody Employee theEmployee) {
Employee dbEmployee = employeeService.save(theEmployee);
return dbEmployee;
}
// DELETE - delete employee
@DeleteMapping("/employees/{employeeId}")
public String deleteEmployee(@PathVariable int employeeId) {
Employee tempEmployee = employeeService.findById(employeeId);
if (tempEmployee == null) {
throw new RuntimeException("Employee id not found - " + employeeId);
}
employeeService.deleteById(employeeId);
return "Deleted employee id - " + employeeId;
}
}Important: Set HTTP request header in Postman:
Content-Type: application/json
Example POST Request Body:
{
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com"
}// PUT Request
{
"id": 5,
"email": "vega.juan@demo.com"
}
// Response - Other fields become null!
{
"id": 5,
"firstName": null,
"lastName": null,
"email": "vega.juan@demo.com"
}PATCH allows partial updates without affecting other fields.
// PATCH Request to /api/employees/5
{
"email": "vega.juan@demo.com"
}
// Response - Other fields preserved!
{
"id": 5,
"firstName": "Juan",
"lastName": "Vega",
"email": "vega.juan@demo.com"
}import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
@RestController
@RequestMapping("/api")
public class EmployeeRestController {
private EmployeeService employeeService;
private ObjectMapper objectMapper;
@Autowired
public EmployeeRestController(EmployeeService theEmployeeService,
ObjectMapper theObjectMapper) {
employeeService = theEmployeeService;
objectMapper = theObjectMapper;
}
// PATCH - partial update
@PatchMapping("/employees/{employeeId}")
public Employee patchEmployee(@PathVariable int employeeId,
@RequestBody Map<String, Object> patchPayload) {
Employee tempEmployee = employeeService.findById(employeeId);
if (tempEmployee == null) {
throw new RuntimeException("Employee id not found - " + employeeId);
}
// Prevent updating the id field
if (patchPayload.containsKey("id")) {
throw new RuntimeException("Employee id not allowed in request body");
}
Employee patchedEmployee = applyPatch(patchPayload, tempEmployee);
Employee dbEmployee = employeeService.save(patchedEmployee);
return dbEmployee;
}
private Employee applyPatch(Map<String, Object> patchPayload,
Employee tempEmployee) {
// Convert employee to JSON node
ObjectNode employeeNode = objectMapper.convertValue(tempEmployee, ObjectNode.class);
// Convert patch payload to JSON node
ObjectNode patchNode = objectMapper.convertValue(patchPayload, ObjectNode.class);
// Merge patch into employee node
employeeNode.setAll(patchNode);
// Convert back to Employee object
return objectMapper.convertValue(employeeNode, Employee.class);
}
}Creating DAOs for multiple entities requires repeating similar code:
// EmployeeDAO
employeeDAO.findAll();
employeeDAO.findById(id);
employeeDAO.save(employee);
employeeDAO.deleteById(id);
// CustomerDAO - Same pattern!
customerDAO.findAll();
customerDAO.findById(id);
customerDAO.save(customer);
customerDAO.deleteById(id);Spring Data JPA provides JpaRepository interface with built-in CRUD methods.
Benefits:
- 70%+ reduction in code
- No need for implementation class
- Get CRUD methods for FREE
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
// No code needed! Get these methods for free:
// - findAll()
// - findById()
// - save()
// - deleteById()
// - count()
// - existsById()
// ... and many more
}Type Parameters:
Employee- Entity typeInteger- Primary key type
@Service
public class EmployeeServiceImpl implements EmployeeService {
private EmployeeRepository employeeRepository;
@Autowired
public EmployeeServiceImpl(EmployeeRepository theEmployeeRepository) {
employeeRepository = theEmployeeRepository;
}
@Override
public List<Employee> findAll() {
return employeeRepository.findAll();
}
@Override
public Employee findById(int theId) {
Optional<Employee> result = employeeRepository.findById(theId);
Employee theEmployee = null;
if (result.isPresent()) {
theEmployee = result.get();
} else {
throw new RuntimeException("Did not find employee id - " + theId);
}
return theEmployee;
}
@Override
public Employee save(Employee theEmployee) {
return employeeRepository.save(theEmployee);
}
@Override
public void deleteById(int theId) {
employeeRepository.deleteById(theId);
}
}Before Spring Data JPA:
- 2 files
- 30+ lines of code
After Spring Data JPA:
- 1 file
- 3 lines of code!
Spring Data REST builds on Spring Data JPA to create REST APIs with ZERO CODE!
- Spring Data REST scans for
JpaRepository - Exposes REST endpoints automatically
- Supports full CRUD operations
Step 1: Add Maven Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>That's it! NO CODING REQUIRED!
For EmployeeRepository:
POST /employees - Create new employee
GET /employees - Get all employees
GET /employees/{id} - Get single employee
PUT /employees/{id} - Update employee
PATCH /employees/{id} - Partial update
DELETE /employees/{id} - Delete employee
Endpoint naming: Spring Data REST uses pluralized form of entity name.
BEFORE Spring Data REST:
┌─────────────────┐
│ REST Controller │ ← 100+ lines of code
└────────┬────────┘
v
┌─────────────────┐
│ Service │
└────────┬────────┘
v
┌─────────────────┐
│ JpaRepository │
└─────────────────┘
AFTER Spring Data REST:
┌─────────────────┐
│ JpaRepository │ ← REST endpoints created automatically!
└─────────────────┘
Spring Data REST responses are HATEOAS compliant.
HATEOAS = Hypermedia as the Engine of Application State
Example Response:
{
"firstName": "Avani",
"lastName": "Gupta",
"email": "avani@myapp.com",
"_links": {
"self": {
"href": "http://localhost:8080/employees/3"
},
"employee": {
"href": "http://localhost:8080/employees/3"
}
}
}{
"_embedded": {
"employees": [
{
"firstName": "Leslie",
"lastName": "Andrews",
...
},
...
]
},
"page": {
"size": 20,
"totalElements": 5,
"totalPages": 1,
"number": 0
}
}Problem: Spring Data REST uses simple pluralization (adds "s")
Employee→/employees✅Goose→/gooses❌ (should be/geese)Person→/persons❌ (should be/people)
Solution: Use @RepositoryRestResource annotation
@RepositoryRestResource(path="members")
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
// Endpoint: /members instead of /employees
}Default: 20 items per page
Navigate pages:
http://localhost:8080/employees?page=0 (first page)
http://localhost:8080/employees?page=1 (second page)
Note: Pages are zero-based!
File: application.properties
# Base path for all REST endpoints
spring.data.rest.base-path=/magic-api
# Default page size
spring.data.rest.default-page-size=50
# Maximum page size
spring.data.rest.max-page-size=100Access endpoints at:
http://localhost:8080/magic-api/employees
Sort by entity property names:
# Sort by lastName (ascending - default)
/employees?sort=lastName
# Sort by firstName, descending
/employees?sort=firstName,desc
# Sort by multiple fields
/employees?sort=lastName,firstName,asc
- REST API exists but no documentation
- Must review source code to find endpoints
- Need Postman or curl to test
Springdoc-OpenAPI automatically:
- Generates API documentation at runtime
- Inspects endpoints based on Spring configurations
- Provides Swagger UI for testing (no Postman needed!)
OpenAPI is an industry-standard format for documenting APIs.
- Website: https://www.openapis.org
Swagger UI is a browser-based interface for interacting with your API.
- Powered by Springdoc-OpenAPI
- Website: https://www.springdoc.org
Step 1: Add Maven Dependency
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>Step 2: Access Swagger UI
Default URL:
http://localhost:8080/swagger-ui/index.html
Step 3: Access API Docs
JSON format:
http://localhost:8080/v3/api-docs
YAML format:
http://localhost:8080/v3/api-docs.yaml
File: application.properties
# Custom path for Swagger UI
springdoc.swagger-ui.path=/my-api-ui.html
# Custom path for API docs
springdoc.api-docs.path=/my-api-docsAccess at:
http://localhost:8080/my-api-ui.html
http://localhost:8080/my-api-docs
http://localhost:8080/my-api-docs.yaml
The Swagger UI provides:
- Interactive testing - Execute API calls directly from browser
- Request/Response examples - See sample data
- Schema definitions - View data models
- Authentication testing - Test secured endpoints
- Multiple content types - JSON, XML, etc.
Good News: Springdoc works seamlessly with Spring Data REST!
Just add the dependency and access Swagger UI - all Spring Data REST endpoints will be documented automatically.
src/main/java/com/myapp/springboot/cruddemo/
├── SpringbootCruddemoApplication.java
├── entity/
│ └── Employee.java
├── dao/
│ ├── EmployeeRepository.java
├── service/
│ ├── EmployeeService.java
│ └── EmployeeServiceImpl.java
├── rest/
│ └── EmployeeRestController.java
└── exception/
└── GlobalExceptionHandler.java
src/main/resources/
├── application.properties
└── schema.sql
File: application.properties
# Database Configuration
spring.datasource.url=jdbc:mysql://localhost:3306/employee_directory
spring.datasource.username=springstudent
spring.datasource.password=springstudent
# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# Spring Data REST Configuration
spring.data.rest.base-path=/api
spring.data.rest.default-page-size=20
spring.data.rest.max-page-size=100
# Springdoc OpenAPI Configuration
springdoc.swagger-ui.path=/swagger-ui.html
springdoc.api-docs.path=/api-docs
# Server Configuration
server.port=8080File: schema.sql
CREATE DATABASE IF NOT EXISTS employee_directory;
USE employee_directory;
DROP TABLE IF EXISTS employee;
CREATE TABLE employee (
id INT NOT NULL AUTO_INCREMENT,
first_name VARCHAR(45) DEFAULT NULL,
last_name VARCHAR(45) DEFAULT NULL,
email VARCHAR(45) DEFAULT NULL,
PRIMARY KEY (id)
);
INSERT INTO employee (first_name, last_name, email) VALUES
('Leslie', 'Andrews', 'leslie@myapp.com'),
('Emma', 'Baumgarten', 'emma@myapp.com'),
('Avani', 'Gupta', 'avani@myapp.com'),
('Yuri', 'Petrov', 'yuri@myapp.com'),
('Juan', 'Vega', 'juan@myapp.com');Download from: https://www.getpostman.com
Free developer plan available!
Method: GET
URL: http://localhost:8080/api/employees
Expected Response:
[
{
"id": 1,
"firstName": "Leslie",
"lastName": "Andrews",
"email": "leslie@myapp.com"
},
{
"id": 2,
"firstName": "Emma",
"lastName": "Baumgarten",
"email": "emma@myapp.com"
}
]Method: GET
URL: http://localhost:8080/api/employees/1
Expected Response:
{
"id": 1,
"firstName": "Leslie",
"lastName": "Andrews",
"email": "leslie@myapp.com"
}Method: POST
URL: http://localhost:8080/api/employees
Headers: Content-Type: application/json
Body (raw JSON):
{
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com"
}Expected Response:
{
"id": 6,
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com"
}Method: PUT
URL: http://localhost:8080/api/employees
Headers: Content-Type: application/json
Body (raw JSON):
{
"id": 6,
"firstName": "John",
"lastName": "Smith",
"email": "john.smith@example.com"
}Method: PATCH
URL: http://localhost:8080/api/employees/6
Headers: Content-Type: application/json
Body (raw JSON):
{
"email": "j.smith@example.com"
}Method: DELETE
URL: http://localhost:8080/api/employees/6
Expected Response:
Deleted employee id - 6
┌──────────────────┐ ┌──────────────────┐
│ │ │ │
│ REST Client │ │ REST Service │
│ (Browser/ │ │ (Spring Boot) │
│ Postman) │ │ │
│ │ │ │
└────────┬─────────┘ └────────┬─────────┘
│ │
│ HTTP Request │
│ (JSON Data) │
├───────────────────────────>│
│ │
│ │
│ HTTP Response │
│ (JSON Data) │
│<───────────────────────────┤
│ │
v v
┌────────────────────────────────────────────┐
│ REST Client (Postman) │
└──────────────────┬─────────────────────────┘
│ HTTP Request/Response
│ (JSON)
┌──────────────────▼─────────────────────────┐
│ REST Controller Layer │
│ @RestController, @RequestMapping │
│ - Handle HTTP requests │
│ - Return HTTP responses │
│ - Exception handling │
└──────────────────┬─────────────────────────┘
│
┌──────────────────▼─────────────────────────┐
│ Service Layer │
│ @Service, @Transactional │
│ - Business logic │
│ - Transaction management │
│ - Integrate multiple DAOs │
└──────────────────┬─────────────────────────┘
│
┌──────────────────▼─────────────────────────┐
│ DAO/Repository Layer │
│ @Repository, JpaRepository │
│ - Database operations (CRUD) │
│ - Data access logic │
└──────────────────┬─────────────────────────┘
│
┌──────────────────▼─────────────────────────┐
│ Database │
│ (MySQL, PostgreSQL, etc.) │
└────────────────────────────────────────────┘
┌─────────────┐
│REST Client │
└──────┬──────┘
│ Request with invalid data
│ (e.g., employee id = 9999)
v
┌─────────────────────┐
│ REST Controller │
│ getEmployee(9999) │
└──────┬──────────────┘
│ Calls service
v
┌─────────────────────┐
│ Service │
│ findById(9999) │
└──────┬──────────────┘
│ Calls repository
v
┌─────────────────────┐
│ Repository │
│ Returns null │
└──────┬──────────────┘
│
v
┌─────────────────────────────────┐
│ Service throws Exception │
│ EmployeeNotFoundException │
└──────┬──────────────────────────┘
│
v
┌─────────────────────────────────┐
│ @ControllerAdvice catches │
│ @ExceptionHandler processes │
│ Returns ResponseEntity with │
│ - Status: 404 NOT_FOUND │
│ - Error message │
│ - Timestamp │
└──────┬──────────────────────────┘
│
v
┌─────────────┐
│REST Client │ Receives error JSON
└─────────────┘
JSON → Java POJO (Deserialization)
══════════════════════════════════
{ public class Employee {
"id": 1, ────────> private int id;
"firstName": Jackson private String firstName;
"John", calls private String lastName;
"lastName": setters
"Doe" public void setId(int id) {...}
} public void setFirstName(String name) {...}
public void setLastName(String name) {...}
}
Java POJO → JSON (Serialization)
══════════════════════════════════
public class Employee { {
private int id = 1; "id": 1,
private String firstName ──> "firstName": "John",
= "John"; Jackson "lastName": "Doe"
private String lastName calls }
= "Doe"; getters
public int getId() {...}
public String getFirstName() {...}
public String getLastName() {...}
}
WITHOUT Spring Data JPA:
════════════════════════
┌─────────────────────────┐
│ EmployeeDAO Interface │
│ - findAll() │
│ - findById() │
│ - save() │
│ - deleteById() │
└────────┬────────────────┘
│
v
┌─────────────────────────┐
│ EmployeeDAOImpl Class │ ← 30+ lines of
│ - EntityManager │ boilerplate code
│ - Manual SQL/JPQL │
│ - Exception handling │
└─────────────────────────┘
WITH Spring Data JPA:
══════════════════════
┌─────────────────────────────────────┐
│ public interface EmployeeRepository│
│ extends JpaRepository< │ ← 3 lines total!
│ Employee, Integer> { │
│ } │ All CRUD methods
└─────────────────────────────────────┘ provided FREE
│
│ Spring Data JPA
│ generates implementation
v
┌─────────────────────────┐
│ AUTO-GENERATED │
│ Implementation with │
│ all CRUD methods │
└─────────────────────────┘
WITHOUT Spring Data REST:
═════════════════════════
┌──────────────────────┐
│ REST Controller │ ← 100+ lines
│ - @GetMapping │ of code
│ - @PostMapping │
│ - @PutMapping │
│ - @DeleteMapping │
│ - Exception Handler │
└──────┬───────────────┘
│
v
┌──────────────────────┐
│ Service │
└──────┬───────────────┘
│
v
┌──────────────────────┐
│ JpaRepository │
└──────────────────────┘
WITH Spring Data REST:
══════════════════════
┌──────────────────────────────┐
│ <dependency> │ ← Just add
│ spring-boot-starter- │ dependency
│ data-rest │
│ </dependency> │
└──────────────────────────────┘
+
┌──────────────────────────────┐
│ JpaRepository │ ← Already exists
└──────────────────────────────┘
║
║ Spring Data REST
║ auto-generates
▼
┌──────────────────────────────┐
│ REST Endpoints (FREE!) │
│ POST /employees │
│ GET /employees │
│ GET /employees/{id} │
│ PUT /employees/{id} │
│ PATCH /employees/{id} │
│ DELETE /employees/{id} │
└──────────────────────────────┘
| Annotation | Purpose | Layer |
|---|---|---|
@RestController |
Marks class as REST controller | Controller |
@RequestMapping |
Maps requests to controller/methods | Controller |
@GetMapping |
Maps HTTP GET requests | Controller |
@PostMapping |
Maps HTTP POST requests | Controller |
@PutMapping |
Maps HTTP PUT requests | Controller |
@PatchMapping |
Maps HTTP PATCH requests | Controller |
@DeleteMapping |
Maps HTTP DELETE requests | Controller |
@PathVariable |
Binds URI path variable to parameter | Controller |
@RequestBody |
Binds HTTP request body to parameter | Controller |
@Service |
Marks class as service component | Service |
@Transactional |
Manages database transactions | Service |
@Repository |
Marks class as DAO component | DAO |
@Entity |
Marks class as JPA entity | Entity |
@Table |
Maps entity to database table | Entity |
@Id |
Marks field as primary key | Entity |
@GeneratedValue |
Auto-generates primary key values | Entity |
@Column |
Maps field to database column | Entity |
@ControllerAdvice |
Global exception handler | Exception |
@ExceptionHandler |
Handles specific exceptions | Exception |
@RepositoryRestResource |
Customizes Spring Data REST | Repository |
✅ Use 3-layer architecture: Controller → Service → DAO ✅ Keep layers separated with clear responsibilities
✅ Apply @Transactional at Service layer, not DAO
✅ Service layer manages transaction boundaries
✅ Use @ControllerAdvice for global exception handling
✅ Return meaningful error messages with proper HTTP status codes
✅ Create custom exception classes
✅ Use proper HTTP methods (GET, POST, PUT, PATCH, DELETE)
✅ Use plural nouns for resources (/employees, not /employee)
✅ Don't include actions in URIs (/employees/3, not /getEmployee/3)
✅ Use path variables for IDs (/employees/{id})
✅ Let Jackson handle serialization/deserialization automatically
✅ Set proper Content-Type headers (application/json)
✅ Use @RequestBody for incoming JSON
✅ Return POJOs directly (Spring converts to JSON)
✅ Use Spring Data JPA to reduce boilerplate code ✅ Consider Spring Data REST for simple CRUD APIs ✅ Use constructor injection over field injection
✅ Use Springdoc-OpenAPI for automatic documentation ✅ Provide Swagger UI for easy testing ✅ Keep documentation up-to-date
✅ Externalize configuration in application.properties
✅ Use proper naming conventions
✅ Configure appropriate page sizes for pagination
Visit: https://start.spring.io
Select:
- Project: Maven
- Language: Java
- Spring Boot: 3.x.x
- Dependencies: Spring Web, Spring Data JPA, MySQL Driver
File: application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/your_database
spring.datasource.username=your_username
spring.datasource.password=your_password
spring.jpa.hibernate.ddl-auto=update@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String firstName;
private String lastName;
private String email;
// constructors, getters, setters
}public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
}@Service
public class EmployeeService {
private EmployeeRepository repository;
@Autowired
public EmployeeService(EmployeeRepository repository) {
this.repository = repository;
}
@Transactional
public List<Employee> findAll() {
return repository.findAll();
}
// other methods...
}@RestController
@RequestMapping("/api")
public class EmployeeRestController {
private EmployeeService service;
@Autowired
public EmployeeRestController(EmployeeService service) {
this.service = service;
}
@GetMapping("/employees")
public List<Employee> findAll() {
return service.findAll();
}
// other endpoints...
}mvn spring-boot:runGET http://localhost:8080/api/employees
- Spring Framework: https://spring.io/projects/spring-framework
- Spring Boot: https://spring.io/projects/spring-boot
- Spring Data JPA: https://spring.io/projects/spring-data-jpa
- Spring Data REST: https://spring.io/projects/spring-data-rest
- Jackson: https://github.com/FasterXML/jackson-databind
- Springdoc-OpenAPI: https://springdoc.org
- Postman: https://www.getpostman.com
- Spring Initializr: https://start.spring.io
- JpaRepository JavaDoc: https://docs.spring.io/spring-data/jpa/docs/current/api/
- Spring Boot Properties: https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html
- HTTP Status Codes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
| Code | Name | Usage |
|---|---|---|
| 200 | OK | Successful request, returning data |
| 201 | Created | Resource successfully created |
| 204 | No Content | Successful request, no body to return |
| Code | Name | Usage |
|---|---|---|
| 301 | Moved Permanently | Resource moved to new URL |
| 304 | Not Modified | Resource hasn't changed since last request |
| Code | Name | Usage |
|---|---|---|
| 400 | Bad Request | Invalid request parameters |
| 401 | Unauthorized | Missing authentication |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource doesn't exist |
| 405 | Method Not Allowed | HTTP method not supported for endpoint |
| 409 | Conflict | Request conflicts with current state |
| 422 | Unprocessable Entity | Validation errors in request |
| Code | Name | Usage |
|---|---|---|
| 500 | Internal Server Error | Unexpected server error |
| 503 | Service Unavailable | Server temporarily unavailable |
// ✅ GOOD - Use nouns for resources, not verbs
GET /api/employees // Get all employees
GET /api/employees/1 // Get employee by ID
POST /api/employees // Create new employee
PUT /api/employees/1 // Update employee
DELETE /api/employees/1 // Delete employee
// ❌ AVOID - Using verbs in URLs
GET /api/getEmployees
POST /api/createEmployee
PUT /api/updateEmployee/1
DELETE /api/deleteEmployee/1Strategy 1: URL Path Versioning
/api/v1/employees
/api/v2/employees
Strategy 2: Request Header Versioning
Headers:
Accept: application/vnd.myapp.v1+json
Accept: application/vnd.myapp.v2+json
Strategy 3: Query Parameter Versioning
/api/employees?version=1
/api/employees?version=2
Problem: Content-Type header incorrect
415 Unsupported Media Type
The request entity has a media type which the server does not support
Solution:
# Ensure Content-Type is set
curl -X POST http://localhost:8080/api/employees \
-H "Content-Type: application/json" \
-d '{"firstName":"John","lastName":"Doe"}'Problem: JSON format doesn't match entity fields
// ❌ WRONG - Date format mismatch
POST /api/employees
{"firstName":"John","hireDate":"2023-10-15"} // String instead of proper format
// ✅ CORRECT - With proper date format
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate hireDate;Problem: Two entities reference each other infinitely
StackOverflowError: Stack overflow
Solution:
// Employee.java
@Entity
public class Employee {
@ManyToOne
@JsonBackReference // Prevents serialization of parent
private Department department;
}
// Department.java
@Entity
public class Department {
@OneToMany(mappedBy = "department")
@JsonManagedReference // Allows serialization of children
private List<Employee> employees;
}Problem: LocalDateTime serialization inconsistent
// ❌ WRONG
private LocalDateTime createdAt;
// ✅ CORRECT - Specify format
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime createdAt;
// Or use Jackson configuration
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000", "http://example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
};
}
}@RestController
@RequestMapping("/api/employees")
public class EmployeeController {
@PostMapping
public ResponseEntity<Employee> createEmployee(@Valid @RequestBody Employee employee) {
// Validation happens automatically
return ResponseEntity.status(HttpStatus.CREATED).body(employeeService.save(employee));
}
}
// Entity with validation
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@NotBlank(message = "First name is required")
@Size(min = 2, max = 50, message = "First name must be 2-50 characters")
private String firstName;
@Email(message = "Email must be valid")
private String email;
@Min(value = 0, message = "Salary must be positive")
private Double salary;
}@RestController
@RequestMapping("/api/employees")
public class EmployeeController {
@GetMapping
public Page<Employee> getEmployees(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "ASC") Sort.Direction direction
) {
Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortBy));
return employeeRepository.findAll(pageable);
}
}
// Usage
// GET /api/employees?page=0&size=20&sortBy=firstName&direction=DESC@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
@Query("SELECT e FROM Employee e WHERE e.department.id = :deptId")
Page<Employee> findByDepartment(@Param("deptId") Integer deptId, Pageable pageable);
}@WebMvcTest(EmployeeController.class)
public class EmployeeControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private EmployeeService employeeService;
@Test
public void testGetAllEmployees() throws Exception {
List<Employee> employees = Arrays.asList(
new Employee(1, "John", "Doe"),
new Employee(2, "Jane", "Smith")
);
when(employeeService.findAll()).thenReturn(employees);
mockMvc.perform(get("/api/employees"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].firstName", is("John")));
}
@Test
public void testCreateEmployee() throws Exception {
Employee newEmployee = new Employee(null, "Bob", "Wilson");
Employee savedEmployee = new Employee(3, "Bob", "Wilson");
when(employeeService.save(any(Employee.class))).thenReturn(savedEmployee);
mockMvc.perform(post("/api/employees")
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().writeValueAsString(newEmployee)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id", is(3)));
}
}@SpringBootTest
@AutoConfigureMockMvc
public class EmployeeControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private EmployeeRepository employeeRepository;
@Test
@Transactional
public void testCreateAndRetrieveEmployee() throws Exception {
Employee employee = new Employee(null, "Test", "User");
mockMvc.perform(post("/api/employees")
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().writeValueAsString(employee)))
.andExpect(status().isCreated());
assertTrue(employeeRepository.findByFirstName("Test").isPresent());
}
}-
Use Lazy Loading Carefully: Consider fetching related data when needed
-
Implement Caching:
@Cacheable("employees") public List<Employee> getAllEmployees() { return employeeRepository.findAll(); }
-
Use DTOs to Avoid N+1 Queries:
@GetMapping public List<EmployeeDTO> getEmployees() { return employeeService.getAllAsDTO(); }
-
Add Compression:
server.compression.enabled=true server.compression.min-response-size=1024
-
Use ResponseEntity for Better Control:
@GetMapping("/{id}") public ResponseEntity<Employee> getEmployee(@PathVariable Integer id) { return employeeRepository.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); }
Happy Coding! 🚀