#การพัฒนา REST API ด้วย Micronaut

Micronaut เป็น JVM framework สำหรับพัฒนา Web Application, REST API และ Microservices โดยออกแบบให้ทำงานจำนวนมากในช่วง compile time แทนการพึ่ง reflection ใน runtime มากเกินไป จึงเหมาะกับระบบที่ให้ความสำคัญกับ startup time, memory footprint, cloud-native deployment และ GraalVM Native Image

บทความนี้สาธิตการสร้าง Product REST API ด้วย Java โดยครอบคลุม CRUD, Validation, Micronaut Data JDBC, H2 Database, Flyway และ OpenAPI/Swagger UI

เนื้อหาอ้างอิง Micronaut Framework 5.x โดย ณ วันที่ 21 กันยายน 2026 มี Micronaut Framework 5.1.5 เป็นรุ่นล่าสุดที่ประกาศบนเว็บไซต์ Micronaut และคู่มือ Micronaut 5 ใช้ JDK 21 ขึ้นไป


#1. สิ่งที่จะสร้าง

API ตัวอย่างมี endpoint ดังนี้

Method Endpoint หน้าที่
GET /api/products แสดงสินค้าทั้งหมด
GET /api/products/{id} ค้นหาสินค้าตาม ID
POST /api/products เพิ่มสินค้า
PUT /api/products/{id} แก้ไขสินค้า
DELETE /api/products/{id} ลบสินค้า

ตัวอย่าง JSON

{
  "name": "Mechanical Keyboard",
  "price": 2590.00
}

#2. Architecture

flowchart LR
    Client["Client / Postman / Frontend"]
    Controller["ProductController"]
    Validation["Bean Validation"]
    Repository["ProductRepository"]
    DB[("H2 / PostgreSQL / MySQL")]

    Client -->|"HTTP + JSON"| Controller
    Controller --> Validation
    Controller --> Repository
    Repository --> DB
    DB --> Repository
    Repository --> Controller
    Controller -->|"HTTP Response + JSON"| Client

โครงสร้างหลักแบ่งเป็น

  • Controller รับ HTTP Request และสร้าง HTTP Response
  • DTO รับและตรวจสอบข้อมูลจาก client
  • Entity แทนข้อมูลที่จัดเก็บในฐานข้อมูล
  • Repository ทำ CRUD ผ่าน Micronaut Data
  • Database ตัวอย่างนี้ใช้ H2 เพื่อทดลองได้ง่าย
  • Flyway จัดการ schema migration
  • OpenAPI / Swagger UI สร้างเอกสาร API

#3. เตรียมเครื่องมือ

ควรมีเครื่องมือดังนี้

JDK 21+
Micronaut CLI
Git
IDE เช่น IntelliJ IDEA หรือ VS Code
curl หรือ Postman

ตรวจสอบ Java

java -version

ตรวจสอบ Micronaut CLI

mn --version

#4. สร้าง Project

สร้างโปรเจกต์ด้วย Maven

mn create-app example.micronaut.productapi \
  --features=data-jdbc,jdbc-hikari,h2,serialization-jackson,validation,flyway,openapi,swagger-ui \
  --build=maven \
  --lang=java \
  --test=junit

เข้า directory ของ project

cd productapi

Feature สำคัญที่ใช้คือ

Feature หน้าที่
data-jdbc ใช้ Micronaut Data JDBC
jdbc-hikari Connection Pool ด้วย HikariCP
h2 In-memory database สำหรับทดลอง
serialization-jackson แปลง Java Object ↔ JSON
validation Bean Validation
flyway Database migration
openapi สร้าง OpenAPI specification
swagger-ui สร้าง Swagger UI

สำหรับ production สามารถเปลี่ยน H2 เป็น PostgreSQL หรือ MySQL ได้


#5. โครงสร้าง Project

ตัวอย่างโครงสร้างที่เราจะใช้

productapi/
├── openapi.properties
├── pom.xml
└── src/
    ├── main/
    │   ├── java/
    │   │   └── example/micronaut/productapi/
    │   │       ├── Application.java
    │   │       ├── Product.java
    │   │       ├── ProductController.java
    │   │       ├── ProductRepository.java
    │   │       └── ProductRequest.java
    │   └── resources/
    │       ├── application.properties
    │       └── db/migration/
    │           └── V1__create_products.sql
    └── test/
        └── java/
            └── example/micronaut/productapi/
                └── ProductControllerTest.java

#6. ตั้งค่า Database

ไฟล์

src/main/resources/application.properties

เพิ่ม configuration

micronaut.application.name=product-api

datasources.default.url=jdbc:h2:mem:productdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
datasources.default.username=sa
datasources.default.password=
datasources.default.driver-class-name=org.h2.Driver
datasources.default.dialect=H2

flyway.datasources.default.enabled=true

micronaut.router.static-resources.swagger-ui.paths=classpath:META-INF/swagger/views/swagger-ui
micronaut.router.static-resources.swagger-ui.mapping=/swagger-ui/**

สำหรับ tutorial นี้ H2 จะทำงานแบบ in-memory ทำให้ไม่ต้องติดตั้ง Database Server เพิ่ม


#7. สร้าง Database Migration

สร้างไฟล์

src/main/resources/db/migration/V1__create_products.sql
CREATE TABLE products (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    price DECIMAL(12, 2) NOT NULL
);

Flyway จะรัน migration เมื่อ application เริ่มทำงาน

ข้อดีคือ schema ถูกจัดการเป็น version และสามารถเก็บไว้ใน Git ได้


#8. สร้าง Product Entity

สร้างไฟล์

src/main/java/example/micronaut/productapi/Product.java
package example.micronaut.productapi;

import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.serde.annotation.Serdeable;

import java.math.BigDecimal;

@Serdeable
@MappedEntity("products")
public class Product {

    @Id
    @GeneratedValue
    @Nullable
    private Long id;

    private String name;
    private BigDecimal price;

    public Product() {
    }

    public Product(@Nullable Long id, String name, BigDecimal price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    @Nullable
    public Long getId() {
        return id;
    }

    public void setId(@Nullable Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }
}

Annotation สำคัญ

  • @MappedEntity ระบุว่า class นี้เป็น persistent entity
  • @Id ระบุ primary key
  • @GeneratedValue ให้ database สร้าง ID
  • @Serdeable เปิดให้ Micronaut Serialization แปลง object เป็น/จาก JSON

#9. สร้าง Request DTO และ Validation

ไม่ควรผูก request body เข้ากับ Entity โดยตรงในระบบจริง เพราะ API contract และ database model มีเหตุผลในการเปลี่ยนแปลงต่างกัน

สร้างไฟล์

src/main/java/example/micronaut/productapi/ProductRequest.java
package example.micronaut.productapi;

import io.micronaut.serde.annotation.Serdeable;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

import java.math.BigDecimal;

@Serdeable
public record ProductRequest(
        @NotBlank String name,
        @NotNull @Positive BigDecimal price
) {
}

Validation ที่กำหนดคือ

  • name ต้องไม่เป็นค่าว่าง
  • price ต้องไม่เป็น null
  • price ต้องมากกว่า 0

#10. สร้าง Repository

สร้างไฟล์

src/main/java/example/micronaut/productapi/ProductRepository.java
package example.micronaut.productapi;

import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;

@JdbcRepository(dialect = Dialect.H2)
public interface ProductRepository extends CrudRepository<Product, Long> {
}

การ extends CrudRepository<Product, Long> ทำให้เราได้ method พื้นฐาน เช่น

findAll()
findById(id)
save(entity)
update(entity)
deleteById(id)
existsById(id)
count()

Micronaut Data สร้าง implementation และ query ที่ต้องใช้ในช่วง compile time


#11. สร้าง REST Controller

สร้างไฟล์

src/main/java/example/micronaut/productapi/ProductController.java
package example.micronaut.productapi;

import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Delete;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.Put;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
import io.micronaut.validation.Validated;
import jakarta.validation.Valid;

@Validated
@ExecuteOn(TaskExecutors.BLOCKING)
@Controller("/api/products")
public class ProductController {

    private final ProductRepository repository;

    public ProductController(ProductRepository repository) {
        this.repository = repository;
    }

    @Get
    public Iterable<Product> list() {
        return repository.findAll();
    }

    @Get("/{id}")
    public HttpResponse<Product> findById(Long id) {
        return repository.findById(id)
                .map(HttpResponse::ok)
                .orElseGet(HttpResponse::notFound);
    }

    @Post
    public HttpResponse<Product> create(@Body @Valid ProductRequest request) {
        Product product = new Product(
                null,
                request.name(),
                request.price()
        );

        Product saved = repository.save(product);

        return HttpResponse.created(saved);
    }

    @Put("/{id}")
    public HttpResponse<Product> update(
            Long id,
            @Body @Valid ProductRequest request
    ) {
        return repository.findById(id)
                .map(product -> {
                    product.setName(request.name());
                    product.setPrice(request.price());

                    Product updated = repository.update(product);

                    return HttpResponse.ok(updated);
                })
                .orElseGet(HttpResponse::notFound);
    }

    @Delete("/{id}")
    public HttpResponse<?> delete(Long id) {
        if (!repository.existsById(id)) {
            return HttpResponse.notFound();
        }

        repository.deleteById(id);
        return HttpResponse.noContent();
    }
}

#ทำไมใช้ @ExecuteOn(TaskExecutors.BLOCKING)

Micronaut Data JDBC เป็น blocking I/O ดังนั้นการแยกงาน database ออกจาก event-loop thread ช่วยป้องกันการ block thread ที่ใช้รับ HTTP request

ถ้าต้องการ reactive database access สามารถพิจารณา Micronaut Data R2DBC แทน JDBC


#12. HTTP Method และ Status Code

API ที่ดีควรใช้ HTTP semantics ให้ชัดเจน

Operation Method Success
List GET 200 OK
Find by ID GET 200 OK
Create POST 201 Created
Update PUT 200 OK
Delete DELETE 204 No Content
Resource ไม่พบ - 404 Not Found
Validation ไม่ผ่าน - 400 Bad Request

#13. รัน Application

ด้วย Maven Wrapper

./mvnw mn:run

Windows

mvnw.cmd mn:run

โดยทั่วไป application จะเปิดที่

http://localhost:8080

#14. ทดสอบ API ด้วย curl

#14.1 Create Product

curl -i \
  -X POST http://localhost:8080/api/products \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Mechanical Keyboard",
    "price": 2590.00
  }'

ผลลัพธ์ตัวอย่าง

{
  "id": 1,
  "name": "Mechanical Keyboard",
  "price": 2590.00
}

#14.2 List Products

curl http://localhost:8080/api/products

#14.3 Get Product by ID

curl http://localhost:8080/api/products/1

#14.4 Update Product

curl -i \
  -X PUT http://localhost:8080/api/products/1 \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Mechanical Keyboard Pro",
    "price": 3290.00
  }'

#14.5 Delete Product

curl -i \
  -X DELETE http://localhost:8080/api/products/1

#15. ทดลอง Validation

ส่งข้อมูลไม่ถูกต้อง

curl -i \
  -X POST http://localhost:8080/api/products \
  -H "Content-Type: application/json" \
  -d '{
    "name": "",
    "price": -100
  }'

เพราะ name ถูกกำหนดด้วย @NotBlank และ price ด้วย @Positive request จะไม่ผ่าน validation

แนวทางนี้ช่วยให้ validation rule อยู่ใกล้กับ API contract และลด business logic ที่ซ้ำใน controller


#16. เพิ่ม OpenAPI และ Swagger UI

สร้างไฟล์ที่ root ของ project

openapi.properties

ใส่ค่า

swagger-ui.enabled=true

จากนั้น compile หรือ run application ใหม่

./mvnw clean compile
./mvnw mn:run

เปิด

http://localhost:8080/swagger-ui/index.html

Swagger UI ช่วยให้ทีมพัฒนาและผู้ใช้ API

  • ดู endpoint
  • ดู request/response schema
  • ทดลองส่ง HTTP request
  • ใช้ OpenAPI specification เป็น API contract
  • นำ specification ไป generate client SDK หรือ documentation ต่อได้

#17. เขียน Integration Test

Micronaut มี @MicronautTest สำหรับเริ่ม Application Context และ embedded server ระหว่าง test

ตัวอย่าง

package example.micronaut.productapi;

import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;

import static org.junit.jupiter.api.Assertions.assertEquals;

@MicronautTest
class ProductControllerTest {

    @Inject
    @Client("/")
    HttpClient client;

    @Test
    void canCreateProduct() {
        ProductRequest request =
                new ProductRequest(
                        "Mechanical Keyboard",
                        new BigDecimal("2590.00")
                );

        var response = client.toBlocking().exchange(
                HttpRequest.POST("/api/products", request),
                Product.class
        );

        assertEquals(HttpStatus.CREATED, response.status());
    }
}

รัน test

./mvnw test

#18. เพิ่ม Custom Query

Micronaut Data สามารถสร้าง query จากชื่อ method ได้

ตัวอย่างค้นหาสินค้าจากชื่อ

package example.micronaut.productapi;

import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;

import java.util.List;

@JdbcRepository(dialect = Dialect.H2)
public interface ProductRepository
        extends CrudRepository<Product, Long> {

    List<Product> findByNameContains(String name);
}

เพิ่ม endpoint

@Get("/search{?q}")
public Iterable<Product> search(String q) {
    return repository.findByNameContains(q);
}

เรียก

GET /api/products/search?q=Keyboard

จุดเด่นคือเราไม่ต้องเขียน SQL สำหรับ query พื้นฐานทุกครั้ง


#19. เปลี่ยนจาก H2 เป็น PostgreSQL

H2 เหมาะกับการเรียนรู้และ prototype แต่ production มักใช้ฐานข้อมูลจริง เช่น PostgreSQL

แนวคิดการเปลี่ยนคือ

  1. เปลี่ยน dependency จาก h2 เป็น PostgreSQL driver
  2. เปลี่ยน datasource URL
  3. เปลี่ยน dialect เป็น POSTGRES
  4. เปลี่ยน @JdbcRepository(dialect = Dialect.H2) เป็น Dialect.POSTGRES
  5. ให้ Flyway migration รองรับ SQL ของ PostgreSQL
  6. เก็บ credential ใน environment variable หรือ secret manager

ตัวอย่าง configuration

datasources.default.url=${JDBC_URL:`jdbc:postgresql://localhost:5432/productdb`}
datasources.default.username=${DB_USERNAME:postgres}
datasources.default.password=${DB_PASSWORD:postgres}
datasources.default.driver-class-name=org.postgresql.Driver
datasources.default.dialect=POSTGRES

repository

@JdbcRepository(dialect = Dialect.POSTGRES)
public interface ProductRepository
        extends CrudRepository<Product, Long> {
}

#20. Environment Variable

อย่า hard-code password ใน source code

ตัวอย่าง

datasources.default.url=${JDBC_URL}
datasources.default.username=${DB_USERNAME}
datasources.default.password=${DB_PASSWORD}

Linux/macOS

export JDBC_URL=jdbc:postgresql://localhost:5432/productdb
export DB_USERNAME=postgres
export DB_PASSWORD=secret

Windows PowerShell

$env:JDBC_URL="jdbc:postgresql://localhost:5432/productdb"
$env:DB_USERNAME="postgres"
$env:DB_PASSWORD="secret"

#21. Error Handling

ใน project ที่ใหญ่ขึ้นควรสร้าง exception และ handler แยกจาก controller เช่น

ProductNotFoundException
ProductNotFoundExceptionHandler
DuplicateProductException
DuplicateProductExceptionHandler

เป้าหมายคือให้ error response มีรูปแบบคงที่ เช่น

{
  "code": "PRODUCT_NOT_FOUND",
  "message": "Product 99 was not found"
}

ไม่ควรส่ง stack trace หรือ internal exception message ที่ไม่จำเป็นกลับไปยัง client ใน production


#22. Security

REST API production ควรพิจารณา Micronaut Security เช่น

security-jwt
OAuth 2.0 / OpenID Connect
Role-based access control

ตัวอย่างแนวคิด

POST /login
      |
      v
Authentication
      |
      v
JWT
      |
      v
Authorization: Bearer <token>
      |
      v
Protected REST API

และควรกำหนด

  • HTTPS
  • CORS อย่างเหมาะสม
  • input validation
  • rate limiting ที่ API Gateway / reverse proxy
  • secret management
  • least privilege สำหรับ database account
  • dependency/security scanning

#23. Production Architecture

flowchart TB
    Client["Web / Mobile / External Client"]
    Proxy["Load Balancer / API Gateway"]
    API["Micronaut REST API"]
    DB[("PostgreSQL")]
    Cache[("Redis")]
    Metrics["Metrics / Tracing / Logs"]

    Client --> Proxy
    Proxy --> API
    API --> DB
    API --> Cache
    API --> Metrics

Micronaut เหมาะกับการนำไป deploy ในหลายรูปแบบ เช่น

JAR
Docker
Kubernetes
Serverless
GraalVM Native Image

#24. Dockerfile ตัวอย่าง

เมื่อ build JAR แล้ว สามารถ package เป็น container ได้

FROM eclipse-temurin:21-jre

WORKDIR /app

COPY target/*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

build

./mvnw clean package
docker build -t micronaut-product-api .

run

docker run --rm -p 8080:8080 micronaut-product-api

ชื่อและตำแหน่ง JAR อาจแตกต่างตาม build configuration ของ project ให้ตรวจสอบใน target/ ก่อนสร้าง image


#25. แนวทางจัด Layer สำหรับ Project ขนาดใหญ่

ตัวอย่าง structure ที่เหมาะกับระบบจริงมากขึ้น

src/main/java/example/product/
├── controller/
│   └── ProductController.java
├── dto/
│   ├── ProductCreateRequest.java
│   ├── ProductUpdateRequest.java
│   └── ProductResponse.java
├── entity/
│   └── Product.java
├── repository/
│   └── ProductRepository.java
├── service/
│   └── ProductService.java
├── exception/
│   ├── ProductNotFoundException.java
│   └── ProductNotFoundExceptionHandler.java
└── Application.java

flow

HTTP Request
   ↓
Controller
   ↓
Service
   ↓
Repository
   ↓
Database

Controller ควรรับผิดชอบด้าน HTTP ส่วน Service รับผิดชอบ business logic และ Repository รับผิดชอบ data access


#26. Best Practices

#แยก DTO ออกจาก Entity

ไม่ควร expose database entity เป็น API contract โดยไม่จำเป็น

#ใช้ Migration Tool

ใช้ Flyway หรือ Liquibase แทนการสร้าง schema ด้วยมือใน production

#Validate ทุก Input

ใช้ jakarta.validation กับ request DTO

#จัดการ Blocking I/O

JDBC เป็น blocking ดังนั้นควรจัด execution model ให้เหมาะสม เช่น @ExecuteOn(TaskExecutors.BLOCKING)

#ใช้ HTTP Status Code ให้ถูกต้อง

เช่น 201, 204, 400, 404, 409

#มี API Documentation

ใช้ OpenAPI และ Swagger UI

#ทดสอบหลายระดับ

อย่างน้อยควรมี

Unit Test
Repository Test
Controller / Integration Test
API Test

#ใช้ Testcontainers กับฐานข้อมูลจริงในการทดสอบ

การใช้ PostgreSQL/MySQL ผ่าน Testcontainers ช่วยลดความต่างระหว่าง test environment กับ production

ใช้ Pagination

endpoint ที่คืนข้อมูลจำนวนมากไม่ควร findAll() ตลอดไป ควรใช้ PageableRepository

#เพิ่ม Observability

production ควรมี

Health Check
Metrics
Distributed Tracing
Structured Logging

#27. Micronaut กับ Spring Boot ต่างกันอย่างไรในมุม REST API

ทั้งสอง framework ใช้สร้าง REST API บน JVM ได้ดี แต่แนวทางภายในแตกต่างกันบางส่วน

หัวข้อ Micronaut Spring Boot
Dependency Injection เน้น compile-time ใช้ runtime mechanisms มากกว่าในหลายส่วน
Reflection ลดการพึ่ง reflection ecosystem ดั้งเดิมมีการใช้ reflection มากกว่า
Startup เน้น startup เร็ว ดีขึ้นมากในรุ่นปัจจุบัน
Memory ออกแบบให้ footprint ต่ำ ขึ้นกับ stack และ configuration
Native Image รองรับ GraalVM รองรับ GraalVM เช่นกัน
Ecosystem เล็กกว่า ใหญ่มาก
Cloud/Microservices จุดเด่นหลัก รองรับครบเช่นกัน

การเลือกควรขึ้นกับ ecosystem, team skill, library compatibility, deployment target และ non-functional requirements ของระบบ


#28. Roadmap การเรียนรู้ Micronaut REST API

1. Controller + Routing
        ↓
2. JSON Serialization
        ↓
3. Validation
        ↓
4. Micronaut Data
        ↓
5. Database + Flyway
        ↓
6. Error Handling
        ↓
7. OpenAPI / Swagger UI
        ↓
8. Testing
        ↓
9. Security JWT / OAuth2
        ↓
10. Docker / Kubernetes
        ↓
11. Metrics / Tracing
        ↓
12. GraalVM Native Image

#29. สรุป

การสร้าง REST API ด้วย Micronaut มีองค์ประกอบหลักไม่ต่างจาก modern backend framework อื่น คือ Routing, Validation, Data Access, Error Handling, Testing, Security และ API Documentation

สิ่งที่เป็นจุดเด่นของ Micronaut คือแนวทาง compile-time dependency injection, introspection และ data access generation ซึ่งช่วยลดงานบางส่วนที่มักเกิดใน runtime และเหมาะกับ cloud-native workloads

สำหรับการเริ่มต้น สามารถใช้ stack ดังนี้

Java 21+
Micronaut 5.x
Micronaut HTTP Server
Micronaut Serialization
Bean Validation
Micronaut Data JDBC
Flyway
PostgreSQL
OpenAPI / Swagger UI
JUnit
Docker

เมื่อระบบเติบโต ควรแยก Controller, Service, Repository, DTO และ Entity ออกจากกัน เพิ่ม security, observability, pagination และ integration testing ให้ครบก่อนนำขึ้น production


#References