#튜토리얼: TODO API 만들기 (단계별)

버전: 0.33.0 최종 업데이트: 2026-03-15 적용 대상: ranvier, ranvier-core, ranvier-runtime, ranvier-http, ranvier-std 카테고리: Getting Started

난이도: 초급–중급 소요 시간: ~30분 목표: Ranvier로 완전한 기능의 인메모리 TODO REST API를 단계별로 만들고, 각 체크포인트에서 검증합니다.


#만들게 될 것

다음 엔드포인트를 가진 REST API:

  • GET /todos — 모든 할일 목록 조회
  • POST /todos — 할일 생성
  • GET /todos/:id — 특정 할일 조회
  • PUT /todos/:id — 할일 수정
  • DELETE /todos/:id — 할일 삭제

#설정

ranvier new todo-api --template crud-api
cd todo-api
cargo check   # ✅ Checkpoint 0: project structure is valid

#체크포인트 1: 데이터 모델 정의 (5분)

src/model.rs를 생성하세요:

use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use std::collections::HashMap;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Todo {
    pub id: u64,
    pub title: String,
    pub done: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateTodo {
    pub title: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateTodo {
    pub title: Option<String>,
    pub done: Option<bool>,
}

/// Shared in-memory store — passed as Ranvier Resources
#[derive(Clone)]
pub struct TodoStore {
    pub todos: Arc<Mutex<HashMap<u64, Todo>>>,
    pub next_id: Arc<Mutex<u64>>,
}

impl TodoStore {
    pub fn new() -> Self {
        Self {
            todos: Arc::new(Mutex::new(HashMap::new())),
            next_id: Arc::new(Mutex::new(1)),
        }
    }
}

impl ranvier_core::transition::ResourceRequirement for TodoStore {}

src/main.rs에 추가하세요:

mod model;
cargo check   # ✅ Checkpoint 1: data model compiles

#체크포인트 2: 목록 조회 및 생성 구현 (7분)

src/handlers.rs를 생성하세요:

use crate::model::{CreateTodo, Todo, TodoStore};
use ranvier_core::prelude::*;

#[derive(Clone)]
pub struct ListTodos;

#[async_trait::async_trait]
impl Transition<(), String> for ListTodos {
    type Error = anyhow::Error;
    type Resources = TodoStore;

    async fn run(&self, _: (), store: &TodoStore, _: &mut Bus) -> Outcome<String, anyhow::Error> {
        let todos = store.todos.lock().unwrap();
        let list: Vec<&Todo> = todos.values().collect();
        match serde_json::to_string(&list) {
            Ok(json) => Outcome::Next(json),
            Err(e) => Outcome::Fault(e.into()),
        }
    }
}

#[derive(Clone)]
pub struct CreateTodoHandler;

#[async_trait::async_trait]
impl Transition<(), String> for CreateTodoHandler {
    type Error = anyhow::Error;
    type Resources = TodoStore;

    async fn run(&self, _: (), store: &TodoStore, bus: &mut Bus) -> Outcome<String, anyhow::Error> {
        // Read parsed body from Bus (set by HTTP extractor layer)
        let body = bus.read::<CreateTodo>().cloned()
            .unwrap_or_else(|| CreateTodo { title: "Untitled".into() });

        let mut next_id = store.next_id.lock().unwrap();
        let id = *next_id;
        *next_id += 1;

        let todo = Todo { id, title: body.title, done: false };
        store.todos.lock().unwrap().insert(id, todo.clone());

        match serde_json::to_string(&todo) {
            Ok(json) => Outcome::Next(json),
            Err(e) => Outcome::Fault(e.into()),
        }
    }
}
cargo check   # ✅ Checkpoint 2: list and create handlers compile

#체크포인트 3: 라우트 연결 (5분)

src/main.rs를 업데이트하세요:

mod model;
mod handlers;

use handlers::{CreateTodoHandler, ListTodos};
use model::TodoStore;
use ranvier_core::prelude::*;
use ranvier_http::prelude::*;
use ranvier_runtime::Axon;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt::init();

    let store = TodoStore::new();

    let list_todos = Axon::<(), (), anyhow::Error, TodoStore>::new("list-todos")
        .then(ListTodos);
    let create_todo = Axon::<(), (), anyhow::Error, TodoStore>::new("create-todo")
        .then(CreateTodoHandler);

    println!("TODO API running on http://127.0.0.1:3000");

    Ranvier::http::<TodoStore>()
        .bind("127.0.0.1:3000")
        .route_group(
            RouteGroup::new("/todos")
                .get("", list_todos)
                .post("", create_todo),
        )
        .run(store)
        .await
        .map_err(|e| anyhow::anyhow!("{}", e))?;

    Ok(())
}
cargo run   # ✅ Checkpoint 3: server starts
curl http://127.0.0.1:3000/todos
# → []

#체크포인트 4: 할일 생성 및 확인 (3분)

# Create a todo
curl -X POST http://127.0.0.1:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "Learn Ranvier"}'
# → {"id":1,"title":"Learn Ranvier","done":false}

# List todos
curl http://127.0.0.1:3000/todos
# → [{"id":1,"title":"Learn Ranvier","done":false}]

체크포인트 4 완료! 기본 CRUD가 작동합니다.


#다음으로 추가할 기능

기능 힌트
GET /todos/:id bus.read::<PathParams>()에서 :id 읽기
PUT /todos/:id Bus에서 UpdateTodo 읽기 + 스토어 업데이트
DELETE /todos/:id HashMap에서 항목 제거
PostgreSQL에 영속화 ranvier-db 크레이트 + ecosystem_reference_seaorm.md 참조
관측 가능성 추가 tutorial_observability.md 참조

#시연된 핵심 개념

개념 위치
타입이 지정된 Resources TodoStoreAxon 리소스로 전달
공유 상태 TodoStoreArc<Mutex<HashMap>>
핸들러별 Transition 동작별 하나의 구조체, impl Transition
RouteGroup DSL .route_group(RouteGroup::new("/todos")...)
부채널로서의 Bus 파싱된 본문을 위한 bus.read::<CreateTodo>()