Toolmingo
Guides7 min read

Spring Boot Cheat Sheet: REST, JPA, Security & Testing

A complete Spring Boot cheat sheet — annotations, REST controllers, JPA repositories, exception handling, Spring Security, testing with MockMvc, and production patterns.

Spring Boot is the standard way to build production-grade Java applications. This cheat sheet covers everything from project setup and REST controllers to JPA, security, and testing — with copy-ready code for Spring Boot 3.x (Java 17+).

Quick reference — annotations

The annotations you'll use in every Spring Boot project.

Annotation Where Purpose
@SpringBootApplication Main class Enables component scan + auto-configuration
@RestController Class Marks as REST controller; combines @Controller + @ResponseBody
@RequestMapping("/api") Class Base URL prefix for all endpoints
@GetMapping Method HTTP GET handler
@PostMapping Method HTTP POST handler
@PutMapping Method HTTP PUT handler
@PatchMapping Method HTTP PATCH handler
@DeleteMapping Method HTTP DELETE handler
@PathVariable Param Binds {id} from URL
@RequestParam Param Binds ?page=1 query param
@RequestBody Param Deserializes JSON body
@ResponseStatus Method Sets HTTP status code
@Service Class Service-layer bean
@Repository Interface Data-access bean; enables exception translation
@Component Class Generic Spring-managed bean
@Configuration Class Java config class
@Bean Method Registers return value as bean
@Autowired Field/Constructor Dependency injection (prefer constructor)
@Value("${key}") Field Injects property value
@Entity Class JPA entity mapped to table
@Id Field Primary key
@GeneratedValue Field Auto-generated ID
@Column Field Column mapping
@Transactional Method/Class Wraps in database transaction
@ControllerAdvice Class Global exception handler

Project setup

Generate a project at start.spring.io or use the Spring Initializr CLI.

Minimal pom.xml dependencies:

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.3.0</version>
</parent>

<dependencies>
  <!-- Web (REST + embedded Tomcat) -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <!-- JPA + Hibernate -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>

  <!-- PostgreSQL driver -->
  <dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
  </dependency>

  <!-- Validation (Bean Validation / Jakarta) -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
  </dependency>

  <!-- Security -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
  </dependency>

  <!-- Testing -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

Main class:

@SpringBootApplication
public class MyApp {
    public static void main(String[] args) {
        SpringApplication.run(MyApp.class, args);
    }
}

REST controller

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    // Constructor injection (preferred over @Autowired on field)
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public List<UserDto> getAll(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        return userService.findAll(page, size);
    }

    @GetMapping("/{id}")
    public UserDto getById(@PathVariable Long id) {
        return userService.findById(id);  // throws 404 if missing
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public UserDto create(@Valid @RequestBody CreateUserRequest req) {
        return userService.create(req);
    }

    @PutMapping("/{id}")
    public UserDto update(@PathVariable Long id,
                          @Valid @RequestBody UpdateUserRequest req) {
        return userService.update(id, req);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        userService.delete(id);
    }
}

Request/response DTOs with validation:

public record CreateUserRequest(
    @NotBlank String name,
    @Email    String email,
    @Size(min = 8) String password
) {}

public record UserDto(Long id, String name, String email) {}

JPA entity and repository

@Entity
@Table(name = "users")
public class User {

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

    @Column(nullable = false)
    private String name;

    @Column(unique = true, nullable = false)
    private String email;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Post> posts = new ArrayList<>();

    // Constructors, getters, setters or use Lombok @Data
}

Spring Data JPA repository — free CRUD + custom queries:

public interface UserRepository extends JpaRepository<User, Long> {

    // Derived query — Spring generates SQL automatically
    Optional<User> findByEmail(String email);
    List<User> findByNameContainingIgnoreCase(String name);
    boolean existsByEmail(String email);

    // JPQL query
    @Query("SELECT u FROM User u WHERE u.createdAt >= :since")
    List<User> findRecentUsers(@Param("since") LocalDateTime since);

    // Native SQL
    @Query(value = "SELECT * FROM users LIMIT :limit", nativeQuery = true)
    List<User> findTopN(@Param("limit") int limit);

    // Pagination
    Page<User> findAll(Pageable pageable);
}

Service layer:

@Service
@Transactional(readOnly = true)   // default: read-only; override per method
public class UserService {

    private final UserRepository userRepo;

    public UserService(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public UserDto findById(Long id) {
        return userRepo.findById(id)
            .map(u -> new UserDto(u.getId(), u.getName(), u.getEmail()))
            .orElseThrow(() -> new ResourceNotFoundException("User", id));
    }

    @Transactional    // write operation needs full transaction
    public UserDto create(CreateUserRequest req) {
        if (userRepo.existsByEmail(req.email())) {
            throw new ConflictException("Email already registered");
        }
        User user = new User();
        user.setName(req.name());
        user.setEmail(req.email());
        User saved = userRepo.save(user);
        return new UserDto(saved.getId(), saved.getName(), saved.getEmail());
    }
}

Exception handling

// Custom exceptions
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String resource, Long id) {
        super(resource + " not found: " + id);
    }
}
public class ConflictException extends RuntimeException {
    public ConflictException(String msg) { super(msg); }
}

// Global handler — catches exceptions from all controllers
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse(404, ex.getMessage()));
    }

    @ExceptionHandler(ConflictException.class)
    public ResponseEntity<ErrorResponse> handleConflict(ConflictException ex) {
        return ResponseEntity.status(HttpStatus.CONFLICT)
            .body(new ErrorResponse(409, ex.getMessage()));
    }

    // Handles @Valid failures
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(
            MethodArgumentNotValidException ex) {
        String msg = ex.getBindingResult().getFieldErrors().stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .collect(Collectors.joining(", "));
        return ResponseEntity.badRequest().body(new ErrorResponse(400, msg));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAll(Exception ex) {
        return ResponseEntity.internalServerError()
            .body(new ErrorResponse(500, "Internal server error"));
    }
}

public record ErrorResponse(int status, String message) {}

application.yml configuration

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: ${DB_USER}
    password: ${DB_PASS}
  jpa:
    hibernate:
      ddl-auto: validate          # use Flyway/Liquibase in prod; never update
    show-sql: false
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        default_batch_fetch_size: 20   # N+1 fix for collections
  security:
    user:
      name: admin                 # dev only; replace with real auth in prod

server:
  port: 8080

logging:
  level:
    com.myapp: DEBUG
    org.hibernate.SQL: WARN

Spring Security (JWT guard)

@Configuration
@EnableWebSecurity
@EnableMethodSecurity      // enables @PreAuthorize on methods
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())          // disable for stateless API
            .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/users").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

// Method-level security
@Service
public class PostService {
    @PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
    public void deletePost(Long postId, Long userId) { ... }
}

Testing

Slice test — controller only (no DB):

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired MockMvc mvc;
    @MockBean  UserService userService;   // mock the service

    @Test
    void getById_returnsUser() throws Exception {
        given(userService.findById(1L))
            .willReturn(new UserDto(1L, "Alice", "alice@example.com"));

        mvc.perform(get("/api/users/1"))
           .andExpect(status().isOk())
           .andExpect(jsonPath("$.name").value("Alice"))
           .andExpect(jsonPath("$.email").value("alice@example.com"));
    }

    @Test
    void create_invalidBody_returns400() throws Exception {
        mvc.perform(post("/api/users")
               .contentType(MediaType.APPLICATION_JSON)
               .content("{\"name\":\"\",\"email\":\"bad\"}"))
           .andExpect(status().isBadRequest());
    }
}

Repository slice test (real DB, in-memory H2):

@DataJpaTest
class UserRepositoryTest {

    @Autowired UserRepository repo;

    @Test
    void findByEmail_returnsUser() {
        User user = new User();
        user.setName("Bob");
        user.setEmail("bob@example.com");
        repo.save(user);

        Optional<User> found = repo.findByEmail("bob@example.com");
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("Bob");
    }
}

Full integration test:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = Replace.ANY)
class UserIntegrationTest {

    @Autowired TestRestTemplate rest;

    @Test
    void createAndFetchUser() {
        var req = new CreateUserRequest("Alice", "a@b.com", "password123");
        var created = rest.postForObject("/api/users", req, UserDto.class);
        assertThat(created.id()).isNotNull();

        var fetched = rest.getForObject("/api/users/" + created.id(), UserDto.class);
        assertThat(fetched.email()).isEqualTo("a@b.com");
    }
}

Pagination pattern

// Controller
@GetMapping
public Page<UserDto> getAll(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size,
        @RequestParam(defaultValue = "id") String sort) {
    Pageable pageable = PageRequest.of(page, size, Sort.by(sort));
    return userService.findAll(pageable);
}

// Service
public Page<UserDto> findAll(Pageable pageable) {
    return userRepo.findAll(pageable).map(u -> new UserDto(u.getId(), u.getName(), u.getEmail()));
}

// Response shape: { "content": [...], "totalElements": 100, "totalPages": 5, "number": 0 }

Common mistakes

Mistake Problem Fix
@Autowired on field Hard to test; hides dependencies Use constructor injection
ddl-auto: update in prod Schema drift, data loss risk Use Flyway or Liquibase
No @Transactional on writes Saves may not commit Add @Transactional to write methods
Exposing @Entity directly in response Leaks internals, circular JSON, lazy-load issues Always use DTOs
EAGER fetch everywhere N+1 queries, slow endpoints Use LAZY + @EntityGraph when needed
Missing spring.jpa.open-in-view=false Lazy loads in view layer, performance issues Set it false in application.yml
No global exception handler Stack traces leak to clients Add @ControllerAdvice

FAQ

What's the difference between @Controller and @RestController?
@RestController = @Controller + @ResponseBody. Use @RestController for APIs that return JSON. Use @Controller when returning view names (Thymeleaf templates).

When should I use @Transactional(readOnly = true)?
Set it as the class-level default for service classes. Hibernate skips dirty checking on read-only transactions, improving performance. Override individual write methods with plain @Transactional.

What's the N+1 problem and how do I fix it in Spring Data JPA?
N+1 happens when fetching a list of entities and each triggers a separate query for its related collection. Fix it with @EntityGraph, JOIN FETCH in JPQL, or set spring.jpa.properties.hibernate.default_batch_fetch_size=20 in application.yml.

How do I handle database migrations?
Use Flyway: add spring-boot-starter-data-jpa + flyway-core to pom.xml, place SQL scripts in src/main/resources/db/migration/V1__init.sql. Never use ddl-auto: create or update in production.

What's the difference between @MockBean and @Mock?
@MockBean (Spring test) replaces a bean in the Spring context — use in @WebMvcTest / @SpringBootTest. @Mock (Mockito) creates a mock without touching Spring — use in plain unit tests with @ExtendWith(MockitoExtension.class).

How do I add CORS support?

@Configuration
public class CorsConfig {
    @Bean
    public WebMvcConfigurer cors() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**")
                    .allowedOrigins("https://myfrontend.com")
                    .allowedMethods("GET", "POST", "PUT", "DELETE")
                    .allowCredentials(true);
            }
        };
    }
}

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools