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
AI Rules in Modern IDEs: Global and Project-Specific Configurations
AI rules customize AI assistants in modern IDEs like Cursor, Windsurf, and VSCode Copilot. Learn to configure global and project-specific rules for consistent, high-quality code.
GraphQL Cursor Rules: API Query Language
Cursor rules for GraphQL covering schema-first design, resolvers, queries/mutations, fragments, error handling with unions, subscriptions, and Apollo Server patterns.
PyTorch Cursor Rules: Deep Learning Best Practices
Cursor rules for PyTorch covering tensor device management, nn.Module patterns, DataLoader pipelines, training loop conventions, gradient handling, GPU/CUDA optimization, model persistence, and distributed training with DDP.