Skip to content

Latest commit

 

History

History
2879 lines (2243 loc) · 74.5 KB

File metadata and controls

2879 lines (2243 loc) · 74.5 KB

Spring Boot 3 REST API - Complete Guide

📋 Table of Contents


🎯 Introduction

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 Basics

What is 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)

Application Architecture Example

┌─────────────────┐         ┌─────────────────┐
│   Weather App   │────────>│ Weather Service │
│   (CLIENT)      │<────────│   (SERVER)      │
│                 │  REST   │  (External)     │
└─────────────────┘   API   └─────────────────┘

REST API Terminology

All of these terms generally mean the SAME thing:

  • REST API
  • RESTful API
  • REST Web Services
  • RESTful Web Services
  • REST Services
  • RESTful Services

📝 JSON Basics

What is JSON?

JSON = JavaScript Object Notation

  • Lightweight data format for storing and exchanging data (plain text)
  • Language independent
  • Can be used with any programming language

JSON Syntax

{
  "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

JSON Value Types

{
  "id": 14,              // Number: no quotes
  "firstName": "Mario",  // String: in double quotes
  "lastName": "Rossi",   
  "active": true,        // Boolean: true/false
  "courses": null        // null value
}

Nested JSON Objects

{
  "id": 14,
  "firstName": "Mario",
  "lastName": "Rossi",
  "active": true,
  "address": {
    "street": "100 Main St",
    "city": "Philadelphia",
    "state": "Pennsylvania",
    "zip": "19103",
    "country": "USA"
  }
}

JSON Arrays

{
  "id": 14,
  "firstName": "Mario",
  "lastName": "Rossi",
  "active": true,
  "languages": ["Java", "C#", "Python", "Javascript"]
}

🌐 HTTP Basics

REST over HTTP - CRUD Operations

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/Response Flow

┌──────────────┐                    ┌──────────────┐
│              │  HTTP Request      │              │
│  CRM Client  │───────────────────>│  CRM REST    │
│     App      │                    │   Service    │
│              │  HTTP Response     │   (Server)   │
│              │<───────────────────│              │
└──────────────┘                    └──────────────┘

HTTP Request Message Structure

Request line: GET /api/employees HTTP/1.1
Header variables: 
  Host: localhost:8080
  Content-Type: application/json
  
Message body: [request payload]

HTTP Response Message Structure

Response line: HTTP/1.1 200 OK
Header variables:
  Content-Type: application/json
  Content-Length: 1234
  
Message body: [response payload]

HTTP Response Status Codes

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 succeeded
  • 201 Created - Resource created
  • 401 Unauthorized - Authentication required
  • 404 Not Found - Resource not found
  • 500 Internal Server Error - Server error

MIME Content Types

Syntax: type/sub-type

Examples:

  • text/html
  • text/plain
  • application/json
  • application/xml

🎮 Spring REST Controller

Hello World Example

@RestController
@RequestMapping("/test")
public class DemoRestController {
    
    @GetMapping("/hello")
    public String sayHello() {
        return "Hello World!";
    }
}

Access at: http://localhost:8080/test/hello

Development Process

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!";
    }
}

🔄 JSON Data Binding with Jackson

What is Data Binding?

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

Jackson Project

Spring uses the Jackson Project behind the scenes for JSON data binding.

GitHub: https://github.com/FasterXML/jackson-databind

JSON to Java POJO (Deserialization)

{
  "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.

Java POJO to JSON (Serialization)

Jackson calls getter methods on POJO to convert to JSON.

Spring Boot Automatic Integration

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!


💻 Complete REST CRUD Application Examples

Example 1: Employee Entity

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 +
                '}';
    }
}

Example 2: Employee Repository

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);
}

Example 3: Employee Service

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);
    }
}

Example 4: Employee REST Controller

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();
        }
    }
}

Example 5: Global Exception Handler

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;
    }
}

Example 6: Testing REST Endpoints with cURL

# 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/1

Example 7: Unit Tests for REST Controller

File: 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
    }
}

🛣️ Path Variables

Retrieve Single Student by ID

@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

⚠️ Exception Handling

Problem

When requesting an invalid student ID (e.g., 9999), the application returns an ugly error page.

Solution: Custom Exception Handling

┌──────────────┐                    ┌──────────────┐
│              │  /api/students/    │              │
│  REST Client │      9999          │  REST Service│
│              │───────────────────>│              │
│              │                    │ Throw        │
│              │                    │ Exception    │
│              │                    └──────┬───────┘
│              │                           │
│              │                           v
│              │                    ┌──────────────┐
│              │   Error Response   │  Exception   │
│              │<───────────────────│   Handler    │
└──────────────┘                    └──────────────┘

Development Process

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);
    }
}

Global Exception Handling with @ControllerAdvice

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

🎨 REST API Design

API Design Process

  1. Review API requirements
  2. Identify main resource/entity
  3. Use HTTP methods to assign actions

Example: Employee Directory REST API

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

Complete API Design

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

Anti-Patterns (BAD Practice)

DON'T DO THIS:

❌ /api/employeesList
❌ /api/deleteEmployee
❌ /api/addEmployee
❌ /api/updateEmployee

Instead, use HTTP methods to assign actions!

Real-World Examples

PayPal Invoicing API:

GitHub Repositories API:

Salesforce REST API:


🏗️ Real-Time Project Architecture

Three-Layer Architecture

┌─────────────────────────┐
│   Employee REST         │
│   Controller            │ ← Handles HTTP requests/responses
└───────────┬─────────────┘
            │
            v
┌─────────────────────────┐
│   Employee Service      │ ← Business logic layer
└───────────┬─────────────┘
            │
            v
┌─────────────────────────┐
│   Employee DAO          │ ← Data access layer
└───────────┬─────────────┘
            │
            v
       [Database]

Database Setup

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');

💾 DAO Layer Implementation

Employee Entity

@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...
}

DAO Interface

public interface EmployeeDAO {
    
    List<Employee> findAll();
    
    Employee findById(int theId);
    
    Employee save(Employee theEmployee);
    
    void deleteById(int theId);
}

DAO Implementation

@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 Layer Implementation

Why Service Layer?

  • 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    │
└──────────┘  └──────────┘

Service Interface

public interface EmployeeService {
    
    List<Employee> findAll();
    
    Employee findById(int theId);
    
    Employee save(Employee theEmployee);
    
    void deleteById(int theId);
}

Service Implementation

@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.


🎯 REST Controller Implementation

Complete CRUD REST Controller

@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;
    }
}

Sending JSON in POST/PUT Requests

Important: Set HTTP request header in Postman:

Content-Type: application/json

Example POST Request Body:

{
  "firstName": "John",
  "lastName": "Doe",
  "email": "john.doe@example.com"
}

🔄 PATCH - Partial Updates

Problem with PUT

// 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"
}

Solution: Use PATCH

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"
}

PATCH Implementation

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);
    }
}

🚀 Spring Data JPA

The Problem

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);

The Solution: Spring Data JPA

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

Implementation

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 type
  • Integer - Primary key type

Using Repository in Service Layer

@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 vs After

Before Spring Data JPA:

  • 2 files
  • 30+ lines of code

After Spring Data JPA:

  • 1 file
  • 3 lines of code!

⚡ Spring Data REST

The Magic of Spring Data REST

Spring Data REST builds on Spring Data JPA to create REST APIs with ZERO CODE!

How It Works

  1. Spring Data REST scans for JpaRepository
  2. Exposes REST endpoints automatically
  3. Supports full CRUD operations

Setup

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!

Default Endpoints

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.

Architecture Comparison

BEFORE Spring Data REST:
┌─────────────────┐
│ REST Controller │  ← 100+ lines of code
└────────┬────────┘
         v
┌─────────────────┐
│    Service      │
└────────┬────────┘
         v
┌─────────────────┐
│  JpaRepository  │
└─────────────────┘

AFTER Spring Data REST:
┌─────────────────┐
│  JpaRepository  │  ← REST endpoints created automatically!
└─────────────────┘

HATEOAS Support

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"
    }
  }
}

Collection Response with Pagination

{
  "_embedded": {
    "employees": [
      {
        "firstName": "Leslie",
        "lastName": "Andrews",
        ...
      },
      ...
    ]
  },
  "page": {
    "size": 20,
    "totalElements": 5,
    "totalPages": 1,
    "number": 0
  }
}

⚙️ Spring Data REST Configuration

Custom Resource Path

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
}

Pagination Configuration

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!

Configuration Properties

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=100

Access endpoints at:

http://localhost:8080/magic-api/employees

Sorting

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

📚 API Documentation with OpenAPI and Swagger

The Problem

  • REST API exists but no documentation
  • Must review source code to find endpoints
  • Need Postman or curl to test

The Solution: Springdoc-OpenAPI

Springdoc-OpenAPI automatically:

  • Generates API documentation at runtime
  • Inspects endpoints based on Spring configurations
  • Provides Swagger UI for testing (no Postman needed!)

What is OpenAPI?

OpenAPI is an industry-standard format for documenting APIs.

What is Swagger UI?

Swagger UI is a browser-based interface for interacting with your API.

Setup Process

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

Custom Configuration

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-docs

Access at:

http://localhost:8080/my-api-ui.html
http://localhost:8080/my-api-docs
http://localhost:8080/my-api-docs.yaml

Swagger UI Features

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.

Using with Spring Data REST

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.


🎯 Complete Project Example

Project Structure

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

Complete Configuration

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=8080

Database Schema

File: 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');

🧪 Testing with Postman

Installation

Download from: https://www.getpostman.com

Free developer plan available!

Testing REST Endpoints

1. GET All Employees

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"
  }
]

2. GET Single Employee

Method: GET
URL: http://localhost:8080/api/employees/1

Expected Response:

{
  "id": 1,
  "firstName": "Leslie",
  "lastName": "Andrews",
  "email": "leslie@myapp.com"
}

3. POST - Create New Employee

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"
}

4. PUT - Update Employee

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"
}

5. PATCH - Partial Update

Method: PATCH
URL: http://localhost:8080/api/employees/6
Headers: Content-Type: application/json
Body (raw JSON):
{
  "email": "j.smith@example.com"
}

6. DELETE Employee

Method: DELETE
URL: http://localhost:8080/api/employees/6

Expected Response:

Deleted employee id - 6

📊 Architecture Diagrams

1. REST Communication Flow

┌──────────────────┐         ┌──────────────────┐
│                  │         │                  │
│   REST Client    │         │   REST Service   │
│   (Browser/      │         │   (Spring Boot)  │
│    Postman)      │         │                  │
│                  │         │                  │
└────────┬─────────┘         └────────┬─────────┘
         │                            │
         │   HTTP Request             │
         │   (JSON Data)              │
         ├───────────────────────────>│
         │                            │
         │                            │
         │   HTTP Response            │
         │   (JSON Data)              │
         │<───────────────────────────┤
         │                            │
         v                            v

2. Spring Boot REST Architecture

┌────────────────────────────────────────────┐
│           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.)                 │
└────────────────────────────────────────────┘

3. Exception Handling Flow

┌─────────────┐
│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
└─────────────┘

4. Jackson Data Binding

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() {...}
}

5. Spring Data JPA Magic

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       │
└─────────────────────────┘

6. Spring Data REST Magic

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}      │
└──────────────────────────────┘

📝 Key Annotations Summary

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

🎓 Best Practices

1. Layered Architecture

✅ Use 3-layer architecture: Controller → Service → DAO ✅ Keep layers separated with clear responsibilities

2. Transaction Management

✅ Apply @Transactional at Service layer, not DAO ✅ Service layer manages transaction boundaries

3. Exception Handling

✅ Use @ControllerAdvice for global exception handling ✅ Return meaningful error messages with proper HTTP status codes ✅ Create custom exception classes

4. REST API Design

✅ 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})

5. JSON Handling

✅ 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)

6. Code Optimization

✅ Use Spring Data JPA to reduce boilerplate code ✅ Consider Spring Data REST for simple CRUD APIs ✅ Use constructor injection over field injection

7. API Documentation

✅ Use Springdoc-OpenAPI for automatic documentation ✅ Provide Swagger UI for easy testing ✅ Keep documentation up-to-date

8. Configuration

✅ Externalize configuration in application.properties ✅ Use proper naming conventions ✅ Configure appropriate page sizes for pagination


🚀 Quick Start Guide

1. Create Spring Boot Project

Visit: https://start.spring.io

Select:

  • Project: Maven
  • Language: Java
  • Spring Boot: 3.x.x
  • Dependencies: Spring Web, Spring Data JPA, MySQL Driver

2. Configure Database

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

3. Create Entity

@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String firstName;
    private String lastName;
    private String email;
    // constructors, getters, setters
}

4. Create Repository

public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
}

5. Create Service

@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...
}

6. Create REST Controller

@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...
}

7. Run Application

mvn spring-boot:run

8. Test with Postman or Browser

GET http://localhost:8080/api/employees

📚 Additional Resources

Official Documentation

Tools

Reference


📋 HTTP Status Codes Reference

Success Codes (2xx)

Code Name Usage
200 OK Successful request, returning data
201 Created Resource successfully created
204 No Content Successful request, no body to return

Redirection (3xx)

Code Name Usage
301 Moved Permanently Resource moved to new URL
304 Not Modified Resource hasn't changed since last request

Client Errors (4xx)

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

Server Errors (5xx)

Code Name Usage
500 Internal Server Error Unexpected server error
503 Service Unavailable Server temporarily unavailable

🎯 REST API Design Best Practices

URL Naming Conventions

// ✅ 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/1

Versioning Strategies

Strategy 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

⚠️ Common REST API Errors & Solutions

Issue: 415 Unsupported Media Type

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"}'

Issue: 400 Bad Request - JSON Deserialization Error

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;

Issue: Circular Reference in JSON Serialization

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;
}

Issue: DateTime Serialization Errors

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;
    }
}

🔒 Security in REST APIs

CORS Configuration

@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);
            }
        };
    }
}

Input Validation

@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;
}

📊 Pagination & Sorting

Implementing Pagination

@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

Custom Queries with Pagination

@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);
}

🧪 Testing REST APIs

Unit Testing with MockMvc

@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)));
    }
}

Integration Testing

@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());
    }
}

💡 Performance Optimization Tips

  1. Use Lazy Loading Carefully: Consider fetching related data when needed

  2. Implement Caching:

    @Cacheable("employees")
    public List<Employee> getAllEmployees() {
        return employeeRepository.findAll();
    }
  3. Use DTOs to Avoid N+1 Queries:

    @GetMapping
    public List<EmployeeDTO> getEmployees() {
        return employeeService.getAllAsDTO();
    }
  4. Add Compression:

    server.compression.enabled=true
    server.compression.min-response-size=1024
  5. 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! 🚀