#튜토리얼: 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 | TodoStore를 Axon 리소스로 전달 |
| 공유 상태 | TodoStore의 Arc<Mutex<HashMap>> |
| 핸들러별 Transition | 동작별 하나의 구조체, impl Transition |
| RouteGroup DSL | .route_group(RouteGroup::new("/todos")...) |
| 부채널로서의 Bus | 파싱된 본문을 위한 bus.read::<CreateTodo>() |