Loading...
Loading...
Micronaut framework guardrails, patterns, and best practices for AI-assisted development. Use when working with Micronaut projects, or when the user mentions Micronaut. Provides compile-time DI, HTTP server/client, data access, and cloud-native guidelines.
npx skill4agent add ar4mirez/samuel micronautApplies to: Micronaut 4.x, Java 21+, Microservices, Serverless, CLI Applications, IoT
jakarta.inject@Singleton@Prototype@RequestScope@Factory@Requires@Autowired@Controller("/path")@ExecuteOn(TaskExecutors.BLOCKING)@ValidatedHttpResponse<T>@Status(HttpStatus.CREATED)@Operation@ApiResponse@JdbcRepository@MongoRepositoryPageableRepository<Entity, ID>@Query@Transactional@Transactional(readOnly = true)schema-generate: CREATE@Serdeable@NotBlank@Email@SizecomponentModel = "jsr330"ExceptionHandler<E, HttpResponse<T>>@Requires(classes = {...})application.ymlapplication-dev.yml${ENV_VAR:default}@ConfigurationProperties@MicronautTest@MockBean@ExtendWith(MockitoExtension.class)TestPropertyProvidershouldCreateUserWhenEmailIsUnique@ExecuteOn(TaskExecutors.BLOCKING)MonoFluxbuild.gradlemyapp/
├── src/main/
│ ├── java/com/example/
│ │ ├── Application.java # Entry point with @OpenAPIDefinition
│ │ ├── config/ # @Factory, @ConfigurationProperties
│ │ ├── controller/ # @Controller classes
│ │ ├── service/ # Business logic interfaces
│ │ │ └── impl/ # Service implementations
│ │ ├── repository/ # @JdbcRepository interfaces
│ │ ├── domain/ # @MappedEntity classes
│ │ ├── dto/ # @Serdeable records
│ │ ├── mapper/ # MapStruct @Mapper interfaces
│ │ └── exception/ # Custom exceptions + handlers
│ └── resources/
│ ├── application.yml # Main config (with -dev.yml, -prod.yml overrides)
│ ├── db/migration/ # Flyway SQL files
│ └── logback.xml
├── src/test/java/com/example/ # Unit + integration tests
├── build.gradle # Micronaut Gradle plugin
└── settings.gradleservice/impl/dto/@Serdeablemapper/exception/ExceptionHandler@Singleton
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
// Constructor injection -- resolved at compile time
}@Factory
public class SecurityBeans {
@Singleton
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
}@Singleton
@Requires(env = "production")
public class ProductionEmailService implements EmailService { }
@Singleton
@Requires(env = "dev")
public class MockEmailService implements EmailService { }@Controller("/api/users")
@Validated
@RequiredArgsConstructor
@ExecuteOn(TaskExecutors.BLOCKING)
@Secured(SecurityRule.IS_AUTHENTICATED)
@Tag(name = "Users", description = "User management endpoints")
public class UserController {
private final UserService userService;
@Post
@Status(HttpStatus.CREATED)
@Operation(summary = "Create a new user")
@ApiResponse(responseCode = "201", description = "User created")
@ApiResponse(responseCode = "409", description = "Email already exists")
public HttpResponse<UserResponse> createUser(@Body @Valid UserRequest request) {
UserResponse user = userService.createUser(request);
return HttpResponse.created(user)
.headers(h -> h.location(URI.create("/api/users/" + user.id())));
}
@Get("/{id}")
@Operation(summary = "Get user by ID")
public UserResponse getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
@Get
@Operation(summary = "List users with pagination")
public PageResponse<UserResponse> getAllUsers(
@QueryValue(defaultValue = "0") int page,
@QueryValue(defaultValue = "20") int size) {
return userService.getAllUsers(page, size);
}
@Put("/{id}")
public UserResponse updateUser(@PathVariable Long id, @Body @Valid UserRequest request) {
return userService.updateUser(id, request);
}
@Delete("/{id}")
@Status(HttpStatus.NO_CONTENT)
@Secured({"ROLE_ADMIN"})
public void deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
}
}@Serdeable
@MappedEntity("users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(GeneratedValue.Type.AUTO)
private Long id;
@Column("email")
private String email;
@Column("password")
private String password;
@DateCreated
@Column("created_at")
private Instant createdAt;
@DateUpdated
@Column("updated_at")
private Instant updatedAt;
}@JdbcRepository(dialect = Dialect.POSTGRES)
public interface UserRepository extends PageableRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
List<User> findByActiveTrue();
Page<User> findByRole(String role, Pageable pageable);
@Query("UPDATE User u SET u.active = :active WHERE u.id = :id")
void updateActiveStatus(Long id, boolean active);
}@Serdeable
public record UserRequest(
@NotBlank @Email @Size(max = 255) String email,
@NotBlank @Size(min = 8, max = 100) String password,
@NotBlank @Size(max = 100) String firstName,
@NotBlank @Size(max = 100) String lastName
) {}
@Serdeable
public record UserResponse(
Long id, String email, String firstName, String lastName,
Boolean active, Instant createdAt, Instant updatedAt
) {}
@Serdeable
public record PageResponse<T>(
List<T> content, int page, int size,
long totalElements, int totalPages, boolean first, boolean last
) {
public static <T> PageResponse<T> of(List<T> content, int page, int size, long total) {
int totalPages = (int) Math.ceil((double) total / size);
return new PageResponse<>(content, page, size, total, totalPages,
page == 0, page >= totalPages - 1);
}
}micronaut:
application:
name: myapp
server:
port: 8080
security:
authentication: bearer
token:
jwt:
signatures:
secret:
generator:
secret: ${JWT_SECRET}
jws-algorithm: HS256
intercept-url-map:
- pattern: /health/**
access: [isAnonymous()]
- pattern: /api/**
access: [isAuthenticated()]
datasources:
default:
url: jdbc:postgresql://localhost:5432/myapp
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:postgres}
flyway:
datasources:
default:
enabled: true
locations: classpath:db/migration
endpoints:
health: { enabled: true, sensitive: false }
prometheus: { enabled: true, sensitive: false }public class ResourceNotFoundException extends RuntimeException {
private final String resourceName;
private final String fieldName;
private final Object fieldValue;
public ResourceNotFoundException(String resource, String field, Object value) {
super(String.format("%s not found with %s: '%s'", resource, field, value));
this.resourceName = resource;
this.fieldName = field;
this.fieldValue = value;
}
// getters
}@Singleton
@Produces
@Requires(classes = {ResourceNotFoundException.class, ExceptionHandler.class})
public class ResourceNotFoundExceptionHandler
implements ExceptionHandler<ResourceNotFoundException, HttpResponse<ErrorResponse>> {
@Override
public HttpResponse<ErrorResponse> handle(HttpRequest request, ResourceNotFoundException ex) {
ErrorResponse error = ErrorResponse.of(404, "Not Found", ex.getMessage(), request.getPath());
return HttpResponse.notFound(error);
}
}@ExtendWith(MockitoExtension.class)
@DisplayName("UserService")
class UserServiceTest {
@Mock private UserRepository userRepository;
@Mock private UserMapper userMapper;
private UserServiceImpl userService;
@BeforeEach
void setUp() {
userService = new UserServiceImpl(userRepository, userMapper);
}
@Test
@DisplayName("should create user when email is unique")
void shouldCreateUserWhenEmailIsUnique() {
when(userRepository.existsByEmail("test@example.com")).thenReturn(false);
when(userMapper.toEntity(any())).thenReturn(new User());
when(userRepository.save(any())).thenReturn(new User());
when(userMapper.toResponse(any())).thenReturn(expectedResponse);
UserResponse result = userService.createUser(request);
assertThat(result).isNotNull();
verify(userRepository).save(any());
}
}@MicronautTest
@Testcontainers
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class IntegrationTest implements TestPropertyProvider {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");
@Inject @Client("/") HttpClient client;
@Override
public Map<String, String> getProperties() {
postgres.start();
return Map.of(
"datasources.default.url", postgres.getJdbcUrl(),
"datasources.default.username", postgres.getUsername(),
"datasources.default.password", postgres.getPassword()
);
}
@Test
void healthEndpoint_shouldReturnUp() {
var response = client.toBlocking().exchange(HttpRequest.GET("/health"), String.class);
assertThat(response.status().getCode()).isEqualTo(200);
}
}./gradlew build # Build
MICRONAUT_ENVIRONMENTS=dev ./gradlew run # Run (dev profile)
./gradlew test # Run tests
./gradlew test jacocoTestReport # Tests + coverage report
./gradlew nativeCompile # GraalVM native image
./gradlew dockerBuild # Docker image
./gradlew dockerBuildNative # Native Docker image
./gradlew clean build # Clean build
./gradlew check # Lint (checkstyle/spotbugs)@Serdeable@ExecuteOn(TaskExecutors.BLOCKING)@Transactional@Autowired