DEV Community

Vikash Kumar
Vikash Kumar

Posted on • Originally published at Medium on

Spring Boot Quick Start Guide

A practical guide to building a production-ready Spring Boot REST API with CRUD operations, database integration, monitoring, and deployment.

Table of Contents

  1. Project Setup
  2. REST CRUD Implementation
  3. Database Layer with JPA
  4. Actuator APIs
  5. Logging & Configuration
  6. Deploy & Test

1. Project Setup

Using Spring Initializr

Visit start.spring.io or use CLI:

curl https://start.spring.io/starter.tgz \
  -d dependencies=web,data-jpa,h2,actuator,validation,lombok \
  -d type=maven-project \
  -d javaVersion=21 \
  -d bootVersion=3.2.0 \
  -d groupId=com.gyan \
  -d artifactId=education-app \
  -d name=EducationApp \
  -d packageName=com.gyan.education | tar -xzvf -
Enter fullscreen mode Exit fullscreen mode

Project Structure

education-app/
├── src/main/java/com/gyan/education/
│ ├── EducationAppApplication.java
│ ├── controller/
│ │ └── StudentController.java
│ ├── model/
│ │ └── Student.java
│ ├── repository/
│ │ └── StudentRepository.java
│ ├── service/
│ │ ├── StudentService.java
│ │ └── StudentServiceImpl.java
│ └── exception/
│ ├── ResourceNotFoundException.java
│ └── GlobalExceptionHandler.java
├── src/main/resources/
│ ├── application.yml
│ └── logback-spring.xml
└── pom.xml
Enter fullscreen mode Exit fullscreen mode

Key Dependencies (pom.xml)

<dependencies>
    <!-- Spring Boot Starters -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <!-- Database -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Utils -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

2. REST CRUD Implementation

Entity Model

package com.gyan.education.model;

import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;

@Entity
@Table(name = "students")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 100)
    private String name;

    @Email(message = "Invalid email format")
    @NotBlank(message = "Email is required")
    @Column(unique = true)
    private String email;

    @NotNull(message = "Grade is required")
    @Min(value = 1, message = "Grade must be between 1 and 12")
    @Max(value = 12, message = "Grade must be between 1 and 12")
    private Integer grade;

    @Column(name = "enrollment_date")
    private LocalDateTime enrollmentDate;

    @CreationTimestamp
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;

    @UpdateTimestamp
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
}
Enter fullscreen mode Exit fullscreen mode

Repository Layer

package com.gyan.education.repository;

import com.gyan.education.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
    Optional<Student> findByEmail(String email);
    List<Student> findByGrade(Integer grade);
    boolean existsByEmail(String email);
}
Enter fullscreen mode Exit fullscreen mode

Service Layer

package com.gyan.education.service;

import com.gyan.education.model.Student;
import java.util.List;

public interface StudentService {
    Student createStudent(Student student);
    Student getStudentById(Long id);
    List<Student> getAllStudents();
    Student updateStudent(Long id, Student student);
    void deleteStudent(Long id);
    List<Student> getStudentsByGrade(Integer grade);
}

package com.gyan.education.service;

import com.gyan.education.exception.ResourceNotFoundException;
import com.gyan.education.model.Student;
import com.gyan.education.repository.StudentRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;

@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class StudentServiceImpl implements StudentService {

    private final StudentRepository studentRepository;

    @Override
    public Student createStudent(Student student) {
        log.info("Creating student with email: {}", student.getEmail());

        if (studentRepository.existsByEmail(student.getEmail())) {
            throw new IllegalArgumentException("Email already exists: " + student.getEmail());
        }

        student.setEnrollmentDate(LocalDateTime.now());
        Student savedStudent = studentRepository.save(student);

        log.info("Student created successfully with ID: {}", savedStudent.getId());
        return savedStudent;
    }

    @Override
    @Transactional(readOnly = true)
    public Student getStudentById(Long id) {
        log.debug("Fetching student with ID: {}", id);
        return studentRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Student not found with ID: " + id));
    }

    @Override
    @Transactional(readOnly = true)
    public List<Student> getAllStudents() {
        log.debug("Fetching all students");
        return studentRepository.findAll();
    }

    @Override
    public Student updateStudent(Long id, Student student) {
        log.info("Updating student with ID: {}", id);

        Student existingStudent = getStudentById(id);
        existingStudent.setName(student.getName());
        existingStudent.setEmail(student.getEmail());
        existingStudent.setGrade(student.getGrade());

        Student updatedStudent = studentRepository.save(existingStudent);
        log.info("Student updated successfully with ID: {}", id);

        return updatedStudent;
    }

    @Override
    public void deleteStudent(Long id) {
        log.info("Deleting student with ID: {}", id);

        Student student = getStudentById(id);
        studentRepository.delete(student);

        log.info("Student deleted successfully with ID: {}", id);
    }

    @Override
    @Transactional(readOnly = true)
    public List<Student> getStudentsByGrade(Integer grade) {
        log.debug("Fetching students by grade: {}", grade);
        return studentRepository.findByGrade(grade);
    }
}
Enter fullscreen mode Exit fullscreen mode

Controller Layer

package com.gyan.education.controller;

import com.gyan.education.model.Student;
import com.gyan.education.service.StudentService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api/v1/students")
@RequiredArgsConstructor
@Slf4j
public class StudentController {

    private final StudentService studentService;

    @PostMapping
    public ResponseEntity<Student> createStudent(@Valid @RequestBody Student student) {
        log.info("POST /api/v1/students - Creating student");
        Student created = studentService.createStudent(student);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Student> getStudent(@PathVariable Long id) {
        log.debug("GET /api/v1/students/{}", id);
        Student student = studentService.getStudentById(id);
        return ResponseEntity.ok(student);
    }

    @GetMapping
    public ResponseEntity<List<Student>> getAllStudents(
            @RequestParam(required = false) Integer grade) {
        log.debug("GET /api/v1/students - grade filter: {}", grade);

        List<Student> students = grade != null 
                ? studentService.getStudentsByGrade(grade)
                : studentService.getAllStudents();

        return ResponseEntity.ok(students);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Student> updateStudent(
            @PathVariable Long id,
            @Valid @RequestBody Student student) {
        log.info("PUT /api/v1/students/{}", id);
        Student updated = studentService.updateStudent(id, student);
        return ResponseEntity.ok(updated);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteStudent(@PathVariable Long id) {
        log.info("DELETE /api/v1/students/{}", id);
        studentService.deleteStudent(id);
        return ResponseEntity.noContent().build();
    }
}
Enter fullscreen mode Exit fullscreen mode

Exception Handling

package com.gyan.education.exception;

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

package com.gyan.education.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
        log.error("Resource not found: {}", ex.getMessage());

        ErrorResponse error = ErrorResponse.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpStatus.NOT_FOUND.value())
                .error("Not Found")
                .message(ex.getMessage())
                .build();

        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationErrors(
            MethodArgumentNotValidException ex) {
        log.error("Validation failed: {}", ex.getMessage());

        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach(error -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArgument(IllegalArgumentException ex) {
        log.error("Invalid argument: {}", ex.getMessage());

        ErrorResponse error = ErrorResponse.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpStatus.BAD_REQUEST.value())
                .error("Bad Request")
                .message(ex.getMessage())
                .build();

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
    }

    @lombok.Data
    @lombok.Builder
    static class ErrorResponse {
        private LocalDateTime timestamp;
        private int status;
        private String error;
        private String message;
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Database Layer with JPA

Configuration (application.yml)

spring:
  application:
    name: education-app

  datasource:
    url: jdbc:h2:mem:educationdb
    driver-class-name: org.h2.Driver
    username: sa
    password: 

  h2:
    console:
      enabled: true
      path: /h2-console

  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        use_sql_comments: true
Enter fullscreen mode Exit fullscreen mode

Initial Data (Optional — data.sql)

-- src/main/resources/data.sql
INSERT INTO students (name, email, grade, enrollment_date, created_at, updated_at) 
VALUES 
    ('Rishi S', 'rishi@gyan.com', 4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
    ('Vedika K', 'vedika@gyan.com', 2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
    ('Shiva R', 'shiva@gyan.com', 7, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
Enter fullscreen mode Exit fullscreen mode

JPA Key Features Used

  • Entity Relationships : @OneToMany, @ManyToOne, @ManyToMany (expandable)
  • Auditing : @CreationTimestamp, @UpdateTimestamp
  • Validation : @NotNull, @Email, @Size, @min, @max
  • Queries : Custom query methods (findByEmail, findByGrade)
  • Transactions : @Transactional for data consistency

4. Actuator APIs

Configuration

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,env,loggers,httptrace
      base-path: /actuator
  endpoint:
    health:
      show-details: always
  info:
    env:
      enabled: true

info:
  app:
    name: Education App
    description: Student Management System
    version: 1.0.0
    author: Vikash Kumar
Enter fullscreen mode Exit fullscreen mode

Key Actuator Endpoints

EndpointPurposeURL Health Application health statusGET /actuator/health Info Application informationGET /actuator/info Metrics Performance metricsGET /actuator/metrics Env Environment propertiesGET /actuator/env Loggers View/modify log levelsGET /actuator/loggers

Custom Health Indicator (Optional)

package com.gyan.education.config;

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class DatabaseHealthIndicator implements HealthIndicator {

    @Override
    public Health health() {
        // Add custom health check logic
        boolean databaseIsUp = checkDatabase();

        if (databaseIsUp) {
            return Health.up()
                    .withDetail("database", "Available")
                    .build();
        }
        return Health.down()
                .withDetail("database", "Unavailable")
                .build();
    }

    private boolean checkDatabase() {
        // Implement actual health check
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Logging & Configuration

Logback Configuration (logback-spring.xml)

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <property name="LOGS" value="./logs" />

    <!-- Console Appender -->
    <appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- File Appender -->
    <appender name="RollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOGS}/education-app.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOGS}/archived/education-app-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy 
                class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
    </appender>

    <!-- Package-specific logging -->
    <logger name="com.gyan.education" level="DEBUG" />
    <logger name="org.springframework.web" level="INFO" />
    <logger name="org.hibernate" level="INFO" />

    <root level="INFO">
        <appender-ref ref="Console" />
        <appender-ref ref="RollingFile" />
    </root>

</configuration>
Enter fullscreen mode Exit fullscreen mode

Application Profiles

application-dev.yml

spring:
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true

logging:
  level:
    com.gyan.education: DEBUG
Enter fullscreen mode Exit fullscreen mode

application-prod.yml

spring:
  jpa:
    show-sql: false

logging:
  level:
    com.gyan.education: INFO

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
Enter fullscreen mode Exit fullscreen mode

Running with Profiles

# Development
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev

# Production
java -jar target/education-app-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
Enter fullscreen mode Exit fullscreen mode

6. Deploy & Test

Build the Application

# Maven
./mvnw clean package

# Gradle
./gradlew build
Enter fullscreen mode Exit fullscreen mode

Run Locally

# Using Maven
./mvnw spring-boot:run

# Using JAR
java -jar target/education-app-0.0.1-SNAPSHOT.jar
# Access at: http://localhost:8080
Enter fullscreen mode Exit fullscreen mode

Test with cURL

# Create a student
curl -X POST http://localhost:8080/api/v1/students \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Doe",
    "email": "john@gyan.com",
    "grade": 4
  }'

# Get all students
curl http://localhost:8080/api/v1/students

# Get student by ID
curl http://localhost:8080/api/v1/students/1

# Get students by grade
curl http://localhost:8080/api/v1/students?grade=4

# Update student
curl -X PUT http://localhost:8080/api/v1/students/1 \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Updated",
    "email": "john.updated@school.com",
    "grade": 11
  }'

# Delete student
curl -X DELETE http://localhost:8080/api/v1/students/1

# Check health
curl http://localhost:8080/actuator/health

# View metrics
curl http://localhost:8080/actuator/metrics
Enter fullscreen mode Exit fullscreen mode

Docker Deployment

Dockerfile

FROM eclipse-temurin:21-jdk-alpine
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Enter fullscreen mode Exit fullscreen mode

Build and Run

# Build Docker image
docker build -t education-app:1.0 .

# Run container
docker run -p 8080:8080 education-app:1.0
Enter fullscreen mode Exit fullscreen mode

Unit Testing Example

package com.gyan.education.service;

import com.gyan.education.exception.ResourceNotFoundException;
import com.gyan.education.model.Student;
import com.gyan.education.repository.StudentRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class StudentServiceImplTest {

    @Mock
    private StudentRepository studentRepository;

    @InjectMocks
    private StudentServiceImpl studentService;

    @Test
    void createStudent_Success() {
        Student student = Student.builder()
                .name("Test Student")
                .email("test@school.com")
                .grade(10)
                .build();

        when(studentRepository.existsByEmail(anyString())).thenReturn(false);
        when(studentRepository.save(any(Student.class))).thenReturn(student);

        Student created = studentService.createStudent(student);

        assertNotNull(created);
        assertEquals("Test Student", created.getName());
        verify(studentRepository).save(any(Student.class));
    }

    @Test
    void getStudentById_NotFound() {
        when(studentRepository.findById(1L)).thenReturn(Optional.empty());

        assertThrows(ResourceNotFoundException.class, 
                () -> studentService.getStudentById(1L));
    }
}
Enter fullscreen mode Exit fullscreen mode

Quick Reference

Project Checklist

  • ✅ Spring Boot 3.x with Java 21
  • ✅ RESTful CRUD endpoints
  • ✅ JPA/Hibernate with H2 database
  • ✅ Request validation
  • ✅ Exception handling
  • ✅ Actuator for monitoring
  • ✅ Structured logging
  • ✅ Profile-based configuration
  • ✅ Docker support

Next Steps

  1. Security : Add Spring Security for authentication/authorization
  2. Swagger : Integrate OpenAPI documentation
  3. Caching : Implement Redis/Caffeine caching
  4. Testing : Add integration tests with TestContainers
  5. Production DB : Switch to PostgreSQL/MySQL
  6. Cloud : Deploy to AWS/Azure/GCP

Happy Coding! 🚀

Top comments (0)