---
title: Filling API
description: Mandu의 Filling API로 서버 로직을 체이닝 방식으로 작성합니다. 8단계 lifecycle, loader, action, WebSocket을 지원합니다.
---

Filling은 만두의 "소"입니다. 서버 로직을 체이닝 API로 정의하며, 8단계 lifecycle을 통해 요청의 전체 흐름을 제어합니다.

## 기본 사용법

```typescript
import { Mandu } from "@mandujs/core";

export default Mandu.filling()
  .get(async (ctx) => {
    return ctx.json({ message: "Hello, Mandu!" });
  })
  .post(async (ctx) => {
    const body = await ctx.body<{ name: string }>();
    return ctx.json({ greeting: `안녕하세요, ${body.name}님!` }, 201);
  });
```

## 8단계 Lifecycle

모든 요청은 다음 순서로 처리됩니다.

```
요청 → onRequest → onParse → beforeHandle → Handler → afterHandle → mapResponse → 응답
                                                                                     ↓
                                                                              afterResponse
                                              에러 발생 시 → onError
```

| 단계 | 메서드 | 역할 | 반환값 |
|------|--------|------|--------|
| 1 | `onRequest` | 요청 수신 직후 | void |
| 2 | `onParse` | 본문 파싱 | void |
| 3 | `beforeHandle` | 핸들러 실행 전 (인증/인가) | Response 반환 시 단축 |
| 4 | **Handler** | 비즈니스 로직 (`get`, `post` 등) | Response |
| 5 | `afterHandle` | 핸들러 실행 후 | 수정된 Response |
| 6 | `mapResponse` | 최종 응답 변환 | 변환된 Response |
| 7 | `afterResponse` | 응답 전송 후 (비동기) | void |
| - | `onError` | 에러 발생 시 | 에러 Response |

### Lifecycle 사용 예시

```typescript
Mandu.filling()
  .onRequest((ctx) => {
    console.log(`[${ctx.method}] ${ctx.url}`);
  })
  .beforeHandle((ctx) => {
    const token = ctx.headers.get("Authorization");
    if (!token) {
      return ctx.json({ error: "인증 필요" }, 401);
    }
  })
  .get(async (ctx) => {
    return ctx.json({ data: "보호된 데이터" });
  })
  .afterResponse((ctx) => {
    // 로깅, 메트릭 수집 등 (비동기)
    analytics.track(ctx.method, ctx.url);
  });
```

## Loader (SSR 데이터 로딩)

Loader는 페이지 렌더링 전에 서버에서 데이터를 가져옵니다. 가져온 데이터는 페이지 컴포넌트에 props로 주입됩니다.

```typescript
Mandu.filling()
  .loader(async (ctx) => {
    const posts = await db.getPosts();
    return { posts };
  });
```

### ISR (Incremental Static Regeneration)

`revalidate` 옵션으로 캐시 시간을 설정하면 ISR이 활성화됩니다.

```typescript
// 60초 동안 캐시, 이후 백그라운드에서 재생성
Mandu.filling()
  .loader(
    async (ctx) => ({ posts: await db.getPosts() }),
    { revalidate: 60 }
  );
```

### 태그 기반 캐시 무효화

```typescript
Mandu.filling()
  .loader(
    async (ctx) => ({ posts: await db.getPosts() }),
    { revalidate: 3600, tags: ["posts"] }
  );
```

### 렌더링 모드

| 모드 | 설명 |
|------|------|
| `dynamic` | 매 요청마다 렌더링 (기본값) |
| `isr` | 캐시 후 백그라운드 재생성 |
| `swr` | Stale-While-Revalidate 패턴 |
| `ppr` | Partial Pre-Rendering |

```typescript
Mandu.filling()
  .render("swr", { revalidate: 300, tags: ["blog"] })
  .loader(async (ctx) => ({ posts: await db.getPosts() }));
```

## Action (Named Mutations)

Action은 이름 기반의 mutation 핸들러입니다. POST 요청의 `_action` 파라미터로 디스패치됩니다. Action 완료 후 loader가 등록되어 있으면 자동으로 데이터를 다시 가져옵니다 (revalidation).

```typescript
Mandu.filling()
  .loader(async (ctx) => ({
    todos: await db.getTodos(),
  }))
  .action("create", async (ctx) => {
    const { title } = await ctx.body<{ title: string }>();
    await db.createTodo(title);
    return ctx.ok({ created: true });
  })
  .action("delete", async (ctx) => {
    const { id } = await ctx.body<{ id: string }>();
    await db.deleteTodo(id);
    return ctx.ok({ deleted: true });
  });
```

클라이언트에서 Action을 호출하는 방법은 두 가지입니다.

```tsx
{/* 1. HTML Form */}
<form method="POST">
  <input type="hidden" name="_action" value="create" />
  <input name="title" />
  <button type="submit">추가</button>
</form>

{/* 2. fetch */}
await fetch("/api/todos", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Requested-With": "ManduAction",
  },
  body: JSON.stringify({ _action: "create", title: "새 할일" }),
});
```

fetch 호출 시 응답에는 action 결과와 최신 loader 데이터가 함께 포함됩니다.

```json
{
  "_action": "create",
  "_revalidated": true,
  "actionData": { "created": true },
  "loaderData": { "todos": [/* 최신 목록 */] }
}
```

## Guard (인증/인가)

`guard`는 `beforeHandle`의 별칭으로, 인증과 인가 로직에 사용합니다.

```typescript
Mandu.filling()
  .guard(async (ctx) => {
    const session = await getSession(ctx);
    if (!session) {
      return ctx.json({ error: "Unauthorized" }, 401);
    }
    ctx.set("user", session.user);
  })
  .get(async (ctx) => {
    const user = ctx.get("user");
    return ctx.json({ profile: user });
  });
```

## Middleware

```typescript
// 함수형 guard
Mandu.filling()
  .use(authGuard)
  .use(rateLimitGuard);

// 플러그인 객체 (여러 lifecycle 단계 조합)
Mandu.filling()
  .use(cors({ origin: "https://example.com" }))
  .use(compress());
```

## WebSocket

```typescript
Mandu.filling()
  .ws({
    open(ws) {
      ws.subscribe("chat");
    },
    message(ws, msg) {
      ws.publish("chat", msg);
    },
    close(ws) {
      console.log("Disconnected:", ws.id);
    },
  });
```

## Semantic Slots

Filling에 메타데이터를 추가하여 AI 에이전트와 팀원이 슬롯의 목적을 이해할 수 있게 합니다.

```typescript
Mandu.filling()
  .purpose("사용자 목록 조회 API")
  .description("페이지네이션된 사용자 목록 반환. 관리자 전용.")
  .tags("users", "admin")
  .owner("backend-team")
  .constraints({
    maxLines: 50,
    requiredPatterns: ["input-validation", "error-handling"],
    forbiddenPatterns: ["direct-db-write"],
  })
  .get(async (ctx) => {
    // ...
  });
```

## HTTP 메서드

| 메서드 | 체이닝 함수 |
|--------|-------------|
| `GET` | `.get(handler)` |
| `POST` | `.post(handler)` |
| `PUT` | `.put(handler)` |
| `PATCH` | `.patch(handler)` |
| `DELETE` | `.delete(handler)` |
| `HEAD` | `.head(handler)` |
| `OPTIONS` | `.options(handler)` |
| 모든 메서드 | `.all(handler)` |

## Method Override

HTML form은 GET과 POST만 지원합니다. Mandu는 `_method` 파라미터를 통한 method override를 지원합니다.

```html
<form method="POST">
  <input type="hidden" name="_method" value="DELETE" />
  <button type="submit">삭제</button>
</form>
```

감지 우선순위: `X-HTTP-Method-Override` 헤더 > `?_method` 쿼리 > body `_method` 필드.
