Spring Boot Cursor Rules: Java Backend Framework
Cursor rules for Spring Boot covering REST controllers, services, repositories, JPA entities, Bean Validation, Spring Security, and JUnit 5 testing.

Overview
Spring Boot is the dominant JVM framework, powering enterprise APIs with auto-configuration, dependency injection, and a vast ecosystem. These cursor rules enforce layered architecture (controller → service → repository), JPA entity conventions, Bean Validation, Spring Security patterns, and JUnit 5 testing to help AI assistants generate production-grade Java backends.
Note:
Enforces controller/service/repository layering, JPA entity and repository conventions, Bean Validation DTOs, Spring Security with method-level authorization, @Transactional boundaries, and JUnit 5/Mockito testing patterns.
Rules Configuration
---
description: Enforces idiomatic Spring Boot patterns including layered architecture, JPA entities and repositories, Bean Validation, Spring Security, transactional service boundaries, and JUnit 5 testing. Provides guidelines for production-grade Java APIs.
globs: **/*.java,**/*.yml,**/*.properties
---
# Spring Boot Best Practices
You are an expert in Spring Boot, Java backend development, and enterprise API design.
You understand Spring Framework, JPA/Hibernate, microservices patterns, and production deployment.
### Project Structure
- /src/main/java/com/example/<domain> — one package per domain
- controller — @RestController classes, handles HTTP concerns
- service — @Service classes, business logic with @Transactional
- repository — @Repository interfaces extending JpaRepository
- dto — request/response POJOs with validation annotations
- entity — @Entity classes mapped to database tables
- exception — custom exception classes and @ControllerAdvice handler
- config — @Configuration classes for beans and security
- /src/main/resources/application.yml — configuration properties
- /src/test — JUnit 5 tests mirroring main source structure
### Controllers & REST
- Annotate with @RestController and @RequestMapping("/api/v1/<resource>")
- Use @GetMapping, @PostMapping, @PutMapping, @DeleteMapping with path variables
- Inject services via constructor injection (no @Autowired on fields)
- Accept DTOs with @Valid @RequestBody for automatic validation
- Return ResponseEntity<T> with appropriate status codes and body
- Never expose entity classes in controller responses — use DTO projections
### Services & Transactions
- Annotate service classes with @Service
- Mark mutating methods with @Transactional
- Use readOnly = true on query-only methods for Hibernate optimization
- Keep services stateless; inject repositories and other services
- Throw custom exceptions (not generic RuntimeException) from service layer
- Return Optional<T> for find-by-id methods that may return null
### JPA & Repositories
- Define entities with @Entity, @Table, @Id, @GeneratedValue
- Use @Column with nullable, length, unique constraints matching the schema
- Define relationships with @ManyToOne, @OneToMany, @ManyToMany — prefer LAZY fetch
- Extend JpaRepository<Entity, Long> for CRUD without boilerplate
- Use @Query with JPQL for complex queries; use nativeQuery = true sparingly
- Avoid bidirectional relationships unless needed — prefer unidirectional
### Validation
- Use Bean Validation on DTOs: @NotBlank, @Email, @Size, @Min, @Max
- Add @Valid on controller method parameters for automatic validation
- Return 400 with field-level error messages on validation failure
- Create custom validators with @Constraint for domain-specific rules
- Use @Validated on service classes for method-level validation
### Security
- Configure SecurityFilterChain bean, not WebSecurityConfigurerAdapter (deprecated)
- Use @PreAuthorize("hasRole('ADMIN')") for method-level authorization
- Use passwordEncoder().encode() for bcrypt password hashing
- Store secrets in application.yml with spring.config.import for vault/external
- Enable CSRF for browser clients; disable for pure API with stateless JWT
### Exception Handling
- Create custom exceptions extending RuntimeException
- Use @ControllerAdvice with @ExceptionHandler for centralized error handling
- Return structured error responses: { error, message, status, timestamp }
- Log exceptions with SLF4J; never expose stack traces in API responses
### Testing
- Use JUnit 5 with @SpringBootTest for integration tests
- Use @WebMvcTest for controller-layer tests with MockMvc
- Use @DataJpaTest for repository-layer tests with in-memory database
- Mock service dependencies with @MockBean
- Use assertEquals, assertThrows, assertTrue for assertions
Installation
Create spring.mdc in your project's .cursor/rules/ directory and paste the configuration above. Cursor and Windsurf both read .cursor/rules/ — Copilot users place it in .github/copilot-instructions.md instead.
Examples
// controller/PostController.java
package com.example.posts.controller;
import com.example.posts.dto.CreatePostRequest;
import com.example.posts.dto.PostResponse;
import com.example.posts.dto.PagedResponse;
import com.example.posts.service.PostService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/posts")
public class PostController {
private final PostService postService;
public PostController(PostService postService) {
this.postService = postService;
}
@GetMapping
@PreAuthorize("isAuthenticated()")
public ResponseEntity<PagedResponse<PostResponse>> getAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return ResponseEntity.ok(postService.findAll(page, size));
}
@PostMapping
@PreAuthorize("isAuthenticated()")
public ResponseEntity<PostResponse> create(@Valid @RequestBody CreatePostRequest request) {
PostResponse created = postService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@GetMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<PostResponse> getById(@PathVariable Long id) {
return ResponseEntity.ok(postService.findById(id));
}
}
// service/PostService.java
package com.example.posts.service;
import com.example.posts.dto.CreatePostRequest;
import com.example.posts.dto.PostResponse;
import com.example.posts.dto.PagedResponse;
import com.example.posts.entity.Post;
import com.example.posts.exception.ResourceNotFoundException;
import com.example.posts.repository.PostRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class PostService {
private final PostRepository postRepository;
public PostService(PostRepository postRepository) {
this.postRepository = postRepository;
}
@Transactional(readOnly = true)
public PagedResponse<PostResponse> findAll(int page, int size) {
Page<Post> postPage = postRepository.findAllByOrderByCreatedAtDesc(PageRequest.of(page, size));
return PagedResponse.from(postPage.map(PostResponse::from));
}
@Transactional
public PostResponse create(CreatePostRequest request) {
Post post = new Post();
post.setTitle(request.title());
post.setBody(request.body());
post.setPublished(request.published() != null ? request.published() : false);
Post saved = postRepository.save(post);
return PostResponse.from(saved);
}
@Transactional(readOnly = true)
public PostResponse findById(Long id) {
Post post = postRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Post not found with id: " + id));
return PostResponse.from(post);
}
}
// entity/Post.java
package com.example.posts.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String title;
@Column(nullable = false, columnDefinition = "TEXT")
private String body;
@Column(nullable = false)
private boolean published = false;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getBody() { return body; }
public void setBody(String body) { this.body = body; }
public boolean isPublished() { return published; }
public void setPublished(boolean published) { this.published = published; }
public LocalDateTime getCreatedAt() { return createdAt; }
}
// repository/PostRepository.java
package com.example.posts.repository;
import com.example.posts.entity.Post;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
Page<Post> findAllByOrderByCreatedAtDesc(Pageable pageable);
}
// dto/PostResponse.java
package com.example.posts.dto;
import com.example.posts.entity.Post;
import java.time.LocalDateTime;
public record PostResponse(
Long id,
String title,
String body,
boolean published,
LocalDateTime createdAt
) {
public static PostResponse from(Post post) {
return new PostResponse(
post.getId(), post.getTitle(), post.getBody(),
post.isPublished(), post.getCreatedAt()
);
}
}
// dto/PagedResponse.java
package com.example.posts.dto;
import org.springframework.data.domain.Page;
import java.util.List;
public record PagedResponse<T>(
List<T> data,
int page,
int total,
int perPage
) {
public static <T> PagedResponse<T> from(Page<T> page) {
return new PagedResponse<>(
page.getContent(),
page.getNumber(),
(int) page.getTotalElements(),
page.getSize()
);
}
}
// dto/CreatePostRequest.java
package com.example.posts.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record CreatePostRequest(
@NotBlank @Size(max = 200) String title,
@NotBlank String body,
Boolean published
) {}
Related Resources
Related Articles
React Native Cursor Rules: Mobile Best Practices Guide
Professional React Native cursor rules guiding component architecture, TypeScript, navigation, testing with Jest and Detox, and performance optimization for cross‑platform iOS and Android apps.
Deno Cursor Rules: Modern JavaScript Runtime
Cursor rules for Deno covering secure-by-default patterns, native TypeScript, ESM imports, Deno KV, Deno Deploy, deno.json config, testing with Deno.test, and Fresh framework conventions.
Vue Cursor Rules: Composition API and SFC Best Practices
Vue cursor rules for Composition API, single-file components, template syntax, reactivity, testing, and performance to produce clean, maintainable applications