跳至主要内容

建立第一個 API

本指南將帶你建立一個簡單的 RESTful API,展示如何使用 AppFuse Server 快速開發。

範例:任務管理 API

我們將建立一個簡單的任務(Task)管理 API,包含:

  • Entity 定義(使用 Lombok 簡化程式碼)
  • Repository 查詢
  • Service 業務邏輯
  • REST API Controller

步驟 1:定義 Entity

entity/task/ 套件中建立 Task.java

package com.yourcompany.yourapp.entity.task;

import io.leandev.appfuse.entity.AuditableEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/// 任務實體
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "task")
public class Task extends AuditableEntity {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(length = 36)
private String id;

@NotBlank
@Size(max = 200)
@Column(nullable = false, length = 200)
private String title;

@Column(length = 2000)
@Size(max = 2000)
private String description;

@Column(nullable = false)
private boolean completed = false;

// ============================================
// 業務方法
// ============================================

/// 標記為完成
public void markAsCompleted() {
this.completed = true;
}

/// 標記為未完成
public void markAsIncomplete() {
this.completed = false;
}
}

說明

  • @Getter, @Setter, @NoArgsConstructor:Lombok 自動生成 getter/setter 和預設建構子
  • AuditableEntity:appfuse 提供的基底類別,自動管理 createdAtupdatedAt 欄位
  • GenerationType.UUID:使用 UUID 作為主鍵,適合分散式系統
  • ///:Java 21+ Markdown 文檔註解

步驟 2:建立 Repository

repository/task/ 套件中建立 TaskRepository.java

package com.yourcompany.yourapp.repository.task;

import com.yourcompany.yourapp.entity.task.Task;
import io.leandev.appfuse.repository.SearchableRepository;

import java.util.List;

public interface TaskRepository extends SearchableRepository<Task, String> {

List<Task> findByCompleted(boolean completed);
}

說明

  • SearchableRepository:appfuse 擴展的 Repository 介面,支援 Filter 條件查詢
  • 泛型參數改為 <Task, String>,對應 UUID 主鍵型別

步驟 3:實作 Service

service/task/ 套件中建立 TaskService.java

package com.yourcompany.yourapp.service.task;

import com.yourcompany.yourapp.entity.task.Task;
import com.yourcompany.yourapp.repository.task.TaskRepository;
import io.leandev.appfuse.exception.NotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

/// 任務服務
@RequiredArgsConstructor
@Service
@Transactional
public class TaskService {

private final TaskRepository taskRepository;

// ============================================
// 查詢方法
// ============================================

@Transactional(readOnly = true)
public List<Task> findAll() {
return taskRepository.findAll();
}

@Transactional(readOnly = true)
public List<Task> findByCompleted(boolean completed) {
return taskRepository.findByCompleted(completed);
}

@Transactional(readOnly = true)
public Optional<Task> findById(@NonNull String id) {
return taskRepository.findById(id);
}

// ============================================
// CRUD 操作
// ============================================

public Task create(@NonNull Task task) {
return taskRepository.save(task);
}

public Task update(@NonNull String id, String title, String description) {
Task task = taskRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Task not found: ${0}", id));

if (title != null) {
task.setTitle(title);
}
if (description != null) {
task.setDescription(description);
}
return taskRepository.save(task);
}

public void delete(@NonNull String id) {
Task task = taskRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Task not found: ${0}", id));
taskRepository.delete(task);
}

public Task markAsCompleted(@NonNull String id) {
Task task = taskRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Task not found: ${0}", id));
task.markAsCompleted();
return taskRepository.save(task);
}
}

說明

  • @RequiredArgsConstructor:Lombok 自動生成 final 欄位的建構子,實現依賴注入
  • @Transactional:類別層級預設所有方法都在交易中執行
  • @Transactional(readOnly = true):查詢方法使用只讀交易,提升效能
  • NotFoundException:appfuse 內建例外,支援 ${0} 格式的參數替換

步驟 4:建立 REST Controller

controller/task/ 套件中建立 TaskController.java

package com.yourcompany.yourapp.controller.task;

import com.yourcompany.yourapp.entity.task.Task;
import com.yourcompany.yourapp.service.task.TaskService;
import io.leandev.appfuse.exception.NotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;
import java.util.List;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/tasks")
public class TaskController {

private final TaskService taskService;

@GetMapping
public List<TaskResponse> findAll(@RequestParam(required = false) Boolean completed) {
List<Task> tasks = completed != null
? taskService.findByCompleted(completed)
: taskService.findAll();
return tasks.stream().map(TaskResponse::from).toList();
}

@GetMapping("/{id}")
public TaskResponse findById(@PathVariable String id) {
Task task = taskService.findById(id)
.orElseThrow(() -> new NotFoundException("Task not found: ${0}", id));
return TaskResponse.from(task);
}

@PostMapping
public ResponseEntity<TaskResponse> create(@RequestBody TaskCreateRequest request) {
Task task = new Task();
task.setTitle(request.title());
task.setDescription(request.description());

Task saved = taskService.create(task);
TaskResponse response = TaskResponse.from(saved);

return ResponseEntity.created(URI.create("/api/tasks/" + saved.getId()))
.body(response);
}

@PutMapping("/{id}")
public TaskResponse update(@PathVariable String id, @RequestBody TaskUpdateRequest request) {
Task task = taskService.update(id, request.title(), request.description());
return TaskResponse.from(task);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable String id) {
taskService.delete(id);
return ResponseEntity.noContent().build();
}

@PatchMapping("/{id}/complete")
public TaskResponse markAsCompleted(@PathVariable String id) {
Task task = taskService.markAsCompleted(id);
return TaskResponse.from(task);
}
}

步驟 5:建立 DTO

controller/task/ 套件中建立請求/回應 DTO:

package com.yourcompany.yourapp.controller.task;

import com.yourcompany.yourapp.entity.task.Task;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

import java.time.LocalDateTime;

// 建立任務請求
record TaskCreateRequest(
@NotBlank @Size(max = 200) String title,
String description
) {}

// 更新任務請求
record TaskUpdateRequest(
@Size(max = 200) String title,
String description
) {}

// 任務回應
record TaskResponse(
String id,
String title,
String description,
boolean completed,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
static TaskResponse from(Task task) {
return new TaskResponse(
task.getId(),
task.getTitle(),
task.getDescription(),
task.isCompleted(),
task.getCreatedAt(),
task.getUpdatedAt()
);
}
}

步驟 6:測試 API

Schema 自動建立

app-server 使用 ddl-auto: update 讓 Hibernate 根據 Entity 定義自動建立/更新資料表。 啟動應用程式時,Hibernate 會自動建立 task 資料表。

生產環境建議使用 Flyway 或 Liquibase 管理 Schema 遷移。

啟動應用程式:

./gradlew bootRun

使用 curl 測試:

# 建立任務
curl -X POST http://localhost:8080/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Learn AppFuse", "description": "Complete the getting started guide"}'

# 回應範例:{"id":"550e8400-e29b-41d4-a716-446655440000","title":"Learn AppFuse",...}

# 查詢所有任務
curl http://localhost:8080/api/tasks

# 查詢單一任務(使用回傳的 UUID)
curl http://localhost:8080/api/tasks/550e8400-e29b-41d4-a716-446655440000

# 更新任務
curl -X PUT http://localhost:8080/api/tasks/550e8400-e29b-41d4-a716-446655440000 \
-H "Content-Type: application/json" \
-d '{"title": "Learn AppFuse Server", "description": "Updated description"}'

# 標記為完成
curl -X PATCH http://localhost:8080/api/tasks/550e8400-e29b-41d4-a716-446655440000/complete

# 刪除任務
curl -X DELETE http://localhost:8080/api/tasks/550e8400-e29b-41d4-a716-446655440000

專案結構總覽

完成後的專案結構:

src/main/java/com/yourcompany/yourapp/
├── entity/
│ └── task/
│ └── Task.java # Entity(使用 Lombok)
├── repository/
│ └── task/
│ └── TaskRepository.java # Repository(擴展 SearchableRepository)
├── service/
│ └── task/
│ └── TaskService.java # Service(使用 Lombok)
├── controller/
│ └── task/
│ ├── TaskController.java # REST Controller
│ ├── TaskCreateRequest.java # Request DTO
│ ├── TaskUpdateRequest.java # Request DTO
│ └── TaskResponse.java # Response DTO
與傳統寫法的差異

本範例使用 appfuse 和 Lombok 簡化程式碼:

  • 不需要手動撰寫 getter/setter、建構子
  • 不需要自訂例外類別,使用 appfuse 內建的 NotFoundException
  • 審計欄位自動管理,繼承 AuditableEntity 即可

下一步

  • Entity 設計最佳實踐 - 學習 Entity 設計模式(待建立)
  • API 設計指南 - RESTful API 設計原則(待建立)
  • 測試策略 - 撰寫單元測試與整合測試(待建立)
  • 安全性 - 添加認證與授權

參考資源