Step 03: Actix-web으로 REST API 만들기
core 90 min

Actix-web으로 REST API 만들기

웹 서버를 시작하고 기본 라우트를 만듭니다.

Execute this step

Run from project root:
cargo run --bin web

Step 3: Actix-web으로 REST API 만들기

학습 목표

  • Actix-web 서버 시작하기
  • HTTP 라우트와 핸들러 만들기
  • JSON으로 요청/응답 주고받기

REST API 설계

| 메서드 | 경로 | 설명 | |--------|------|------| | GET | /books | 전체 도서 목록 | | GET | /books/{id} | 특정 도서 조회 | | POST | /books | 새 도서 추가 | | POST | /books/{id}/borrow | 도서 대여 | | POST | /books/{id}/return | 도서 반납 |

웹 서버 코드

src/bin/web.rs 생성:

1use actix_web::{web, App, HttpServer, HttpResponse, Result};
2use book_manager::{AsyncLibrary, Category, Book};
3use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5
6// 공유 상태
7struct AppState {
8    library: Arc<AsyncLibrary>,
9}
10
11// JSON 요청 타입
12#[derive(Deserialize)]
13struct CreateBookRequest {
14    title: String,
15    author: String,
16    category: String,
17}
18
19#[derive(Deserialize)]
20struct BorrowRequest {
21    borrower: String,
22    due_date: String,
23}
24
25// JSON 응답 타입
26#[derive(Serialize)]
27struct ApiResponse<T> {
28    success: bool,
29    data: Option<T>,
30    message: Option<String>,
31}
32
33impl<T> ApiResponse<T> {
34    fn success(data: T) -> Self {
35        ApiResponse {
36            success: true,
37            data: Some(data),
38            message: None,
39        }
40    }
41    
42    fn error(message: String) -> Self {
43        ApiResponse {
44            success: false,
45            data: None,
46            message: Some(message),
47        }
48    }
49}
50
51// 핸들러 함수들
52async fn health_check() -> HttpResponse {
53    HttpResponse::Ok().json(ApiResponse::success("OK"))
54}
55
56async fn list_books(data: web::Data<AppState>) -> Result<HttpResponse> {
57    let books = data.library.list_all().await;
58    Ok(HttpResponse::Ok().json(ApiResponse::success(books)))
59}
60
61async fn get_book(
62    data: web::Data<AppState>,
63    path: web::Path<u32>,
64) -> Result<HttpResponse> {
65    let id = path.into_inner();
66    
67    match data.library.find_book(id).await {
68        Some(book) => Ok(HttpResponse::Ok().json(ApiResponse::success(book))),
69        None => Ok(HttpResponse::NotFound().json(
70            ApiResponse::<()>::error(format!("책 ID {}를 찾을 수 없습니다", id))
71        )),
72    }
73}
74
75async fn create_book(
76    data: web::Data<AppState>,
77    req: web::Json<CreateBookRequest>,
78) -> Result<HttpResponse> {
79    let category: Category = req.category.as_str().into();
80    
81    let id = data.library.add_book(
82        req.title.clone(),
83        req.author.clone(),
84        category,
85    ).await;
86    
87    Ok(HttpResponse::Created().json(ApiResponse::success(id)))
88}
89
90async fn borrow_book(
91    data: web::Data<AppState>,
92    path: web::Path<u32>,
93    req: web::Json<BorrowRequest>,
94) -> Result<HttpResponse> {
95    let id = path.into_inner();
96    
97    match data.library.borrow_book(
98        id,
99        req.borrower.clone(),
100        req.due_date.clone(),
101    ).await {
102        Ok(_) => Ok(HttpResponse::Ok().json(ApiResponse::success("대여 완료"))),
103        Err(e) => Ok(HttpResponse::BadRequest().json(
104            ApiResponse::<()>::error(e)
105        )),
106    }
107}
108
109async fn return_book(
110    data: web::Data<AppState>,
111    path: web::Path<u32>,
112) -> Result<HttpResponse> {
113    let id = path.into_inner();
114    
115    match data.library.return_book(id).await {
116        Ok(_) => Ok(HttpResponse::Ok().json(ApiResponse::success("반납 완료"))),
117        Err(e) => Ok(HttpResponse::BadRequest().json(
118            ApiResponse::<()>::error(e)
119        )),
120    }
121}
122
123#[actix_web::main]
124async fn main() -> std::io::Result<()> {
125    println!("🚀 웹 서버 시작: http://localhost:8080");
126    
127    // 공유 상태 생성
128    let library = Arc::new(AsyncLibrary::new());
129    
130    // 샘플 데이터 추가
131    library.add_book(
132        "러스트 프로그래밍".to_string(),
133        "스티브".to_string(),
134        Category::Science,
135    ).await;
136    library.add_book(
137        "해리포터".to_string(),
138        "J.K. 롤링".to_string(),
139        Category::Fiction,
140    ).await;
141    
142    let app_state = web::Data::new(AppState { library });
143    
144    HttpServer::new(move || {
145        App::new()
146            .app_data(app_state.clone())
147            .route("/health", web::get().to(health_check))
148            .route("/books", web::get().to(list_books))
149            .route("/books/{id}", web::get().to(get_book))
150            .route("/books", web::post().to(create_book))
151            .route("/books/{id}/borrow", web::post().to(borrow_book))
152            .route("/books/{id}/return", web::post().to(return_book))
153    })
154    .bind(("127.0.0.1", 8080))?
155    .run()
156    .await
157}

API 테스트

1. 서버 실행

cargo run --bin web

2. curl로 테스트

헬스 체크:

curl http://localhost:8080/health

전체 도서 목록:

curl http://localhost:8080/books

특정 도서 조회:

curl http://localhost:8080/books/1

새 도서 추가:

1curl -X POST http://localhost:8080/books \
2  -H "Content-Type: application/json" \
3  -d '{
4    "title": "클린 코드",
5    "author": "로버트 마틴",
6    "category": "nonfiction"
7  }'

도서 대여:

1curl -X POST http://localhost:8080/books/1/borrow \
2  -H "Content-Type: application/json" \
3  -d '{
4    "borrower": "김철수",
5    "due_date": "2026-02-20"
6  }'

도서 반납:

curl -X POST http://localhost:8080/books/1/return

Cargo.toml 수정

1[[bin]]
2name = "cli"
3path = "src/main.rs"
4
5[[bin]]
6name = "web"
7path = "src/bin/web.rs"

체크리스트

  • [ ] Actix-web 서버를 시작했습니다
  • [ ] REST API 라우트를 정의했습니다
  • [ ] JSON 요청을 받아 처리했습니다
  • [ ] JSON 응답을 반환했습니다
  • [ ] curl로 API를 테스트했습니다

다음 단계

Step 4에서는 에러 처리를 개선하고 미들웨어를 추가합니다. 축하합니다! 이제 Rust로 웹 서버를 만들 수 있습니다!

Did you find this helpful? Give it a cheer!