Axum을 사용하여 완전한 기능의 REST API를 구축합니다.
$ cargo new api-server
$ cd api-server
# Cargo.toml
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "sqlite"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = "0.3"
thiserror = "1"
anyhow = "1"
// src/models.rs
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct User {
pub id: i64,
pub username: String,
pub email: String,
pub created_at: String,
}
#[derive(Debug, Deserialize)]
pub struct CreateUser {
pub username: String,
pub email: String,
}
#[derive(Debug, Deserialize)]
pub struct UpdateUser {
pub username: Option<String>,
pub email: Option<String>,
}
// src/db.rs
use sqlx::{sqlite::SqlitePool, Error};
use crate::models::{User, CreateUser, UpdateUser};
pub struct Database {
pool: SqlitePool,
}
impl Database {
pub async fn new(database_url: &str) -> Result<Self, Error> {
let pool = SqlitePool::connect(database_url).await?;
// 테이블 생성
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
"#,
)
.execute(&pool)
.await?;
Ok(Database { pool })
}
pub async fn create_user(&self, user: CreateUser) -> Result<User, Error> {
let user = sqlx::query_as::<_, User>(
r#"
INSERT INTO users (username, email)
VALUES (?, ?)
RETURNING id, username, email, created_at
"#,
)
.bind(&user.username)
.bind(&user.email)
.fetch_one(&self.pool)
.await?;
Ok(user)
}
pub async fn get_user(&self, id: i64) -> Result<Option<User>, Error> {
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = ?")
.bind(id)
.fetch_optional(&self.pool)
.await?;
Ok(user)
}
pub async fn list_users(&self) -> Result<Vec<User>, Error> {
let users = sqlx::query_as::<_, User>("SELECT * FROM users ORDER BY id")
.fetch_all(&self.pool)
.await?;
Ok(users)
}
pub async fn update_user(&self, id: i64, update: UpdateUser) -> Result<User, Error> {
let mut query = String::from("UPDATE users SET ");
let mut updates = vec![];
if let Some(ref username) = update.username {
updates.push(format!("username = '{}'", username));
}
if let Some(ref email) = update.email {
updates.push(format!("email = '{}'", email));
}
query.push_str(&updates.join(", "));
query.push_str(&format!(" WHERE id = {} RETURNING *", id));
let user = sqlx::query_as::<_, User>(&query)
.fetch_one(&self.pool)
.await?;
Ok(user)
}
pub async fn delete_user(&self, id: i64) -> Result<(), Error> {
sqlx::query("DELETE FROM users WHERE id = ?")
.bind(id)
.execute(&self.pool)
.await?;
Ok(())
}
}
// src/error.rs
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ApiError {
#[error("데이터베이스 에러: {0}")]
Database(#[from] sqlx::Error),
#[error("찾을 수 없음: {0}")]
NotFound(String),
#[error("잘못된 요청: {0}")]
BadRequest(String),
#[error("내부 서버 에러")]
Internal,
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
ApiError::Database(ref e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
ApiError::NotFound(ref msg) => (StatusCode::NOT_FOUND, msg.clone()),
ApiError::BadRequest(ref msg) => (StatusCode::BAD_REQUEST, msg.clone()),
ApiError::Internal => (
StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error".to_string(),
),
};
let body = Json(json!({
"error": error_message,
}));
(status, body).into_response()
}
}
// src/handlers.rs
use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use std::sync::Arc;
use crate::{
db::Database,
error::ApiError,
models::{CreateUser, UpdateUser, User},
};
pub async fn create_user(
State(db): State<Arc<Database>>,
Json(payload): Json<CreateUser>,
) -> Result<(StatusCode, Json<User>), ApiError> {
let user = db.create_user(payload).await?;
Ok((StatusCode::CREATED, Json(user)))
}
pub async fn get_user(
State(db): State<Arc<Database>>,
Path(id): Path<i64>,
) -> Result<Json<User>, ApiError> {
let user = db
.get_user(id)
.await?
.ok_or_else(|| ApiError::NotFound(format!("User {} not found", id)))?;
Ok(Json(user))
}
pub async fn list_users(
State(db): State<Arc<Database>>,
) -> Result<Json<Vec<User>>, ApiError> {
let users = db.list_users().await?;
Ok(Json(users))
}
pub async fn update_user(
State(db): State<Arc<Database>>,
Path(id): Path<i64>,
Json(payload): Json<UpdateUser>,
) -> Result<Json<User>, ApiError> {
let user = db.update_user(id, payload).await?;
Ok(Json(user))
}
pub async fn delete_user(
State(db): State<Arc<Database>>,
Path(id): Path<i64>,
) -> Result<StatusCode, ApiError> {
db.delete_user(id).await?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn health_check() -> &'static str {
"OK"
}
// src/main.rs
mod db;
mod error;
mod handlers;
mod models;
use axum::{
routing::{get, post, put, delete},
Router,
};
use std::sync::Arc;
use tower_http::cors::CorsLayer;
use tracing_subscriber;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 로깅 초기화
tracing_subscriber::fmt::init();
// 데이터베이스 연결
let db = Arc::new(db::Database::new("sqlite::memory:").await?);
// 라우터 설정
let app = Router::new()
.route("/health", get(handlers::health_check))
.route("/users", get(handlers::list_users).post(handlers::create_user))
.route(
"/users/:id",
get(handlers::get_user)
.put(handlers::update_user)
.delete(handlers::delete_user),
)
.layer(CorsLayer::permissive())
.with_state(db);
// 서버 시작
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
println!("🚀 Server running on http://localhost:3000");
axum::serve(listener, app).await?;
Ok(())
}
Rust 코드를 WebAssembly로 컴파일하여 브라우저에서 실행합니다.
$ cargo install wasm-pack
$ wasm-pack new wasm-demo
$ cd wasm-demo
# Cargo.toml
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# src/lib.rs
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
#[derive(Serialize, Deserialize)]
pub struct User {
name: String,
age: u32,
}
#[wasm_bindgen]
pub fn process_user(json: &str) -> Result<String, JsValue> {
let user: User = serde_json::from_str(json)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let response = format!("{} is {} years old", user.name, user.age);
Ok(response)
}
$ wasm-pack build --target web
# index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rust WASM Demo</title>
</head>
<body>
<h1>Rust WebAssembly Demo</h1>
<div id="output"></div>
<script type="module">
import init, { greet, fibonacci, process_user } from './pkg/wasm_demo.js';
async function run() {
await init();
// greet 함수 호출
const greeting = greet('World');
console.log(greeting);
// fibonacci 계산
const fib = fibonacci(10);
console.log(`Fibonacci(10) = ${fib}`);
// JSON 처리
const user = JSON.stringify({ name: "Alice", age: 30 });
const result = process_user(user);
console.log(result);
document.getElementById('output').innerHTML = `
<p>${greeting}</p>
<p>Fibonacci(10) = ${fib}</p>
<p>${result}</p>
`;
}
run();
</script>
</body>
</html>
WIA-INTENT와 WIA-OMNI-API를 사용하여 표준화된 인터페이스를 구현합니다.
// src/wia/intent.rs
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Intent {
pub action: String,
pub params: serde_json::Value,
pub context: Option<Context>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Context {
pub user_id: Option<String>,
pub session_id: Option<String>,
pub timestamp: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct IntentResponse {
pub success: bool,
pub data: serde_json::Value,
pub error: Option<String>,
}
pub trait IntentHandler {
fn handle(&self, intent: Intent) -> IntentResponse;
}
// 예제 핸들러
pub struct UserIntentHandler;
impl IntentHandler for UserIntentHandler {
fn handle(&self, intent: Intent) -> IntentResponse {
match intent.action.as_str() {
"user.create" => {
// 사용자 생성 로직
IntentResponse {
success: true,
data: serde_json::json!({"id": 123, "created": true}),
error: None,
}
}
"user.get" => {
// 사용자 조회 로직
IntentResponse {
success: true,
data: serde_json::json!({"id": 123, "name": "Alice"}),
error: None,
}
}
_ => IntentResponse {
success: false,
data: serde_json::Value::Null,
error: Some(format!("Unknown action: {}", intent.action)),
},
}
}
}
// src/wia/omni.rs
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct OmniRequest {
pub endpoint: String,
pub method: String,
pub headers: std::collections::HashMap<String, String>,
pub body: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct OmniResponse {
pub status: u16,
pub headers: std::collections::HashMap<String, String>,
pub body: serde_json::Value,
}
pub async fn call_omni_api(request: OmniRequest) -> Result<OmniResponse, anyhow::Error> {
let client = reqwest::Client::new();
let mut req = match request.method.as_str() {
"GET" => client.get(&request.endpoint),
"POST" => client.post(&request.endpoint),
"PUT" => client.put(&request.endpoint),
"DELETE" => client.delete(&request.endpoint),
_ => return Err(anyhow::anyhow!("Unsupported method: {}", request.method)),
};
for (key, value) in request.headers {
req = req.header(&key, &value);
}
if let Some(body) = request.body {
req = req.json(&body);
}
let response = req.send().await?;
let status = response.status().as_u16();
let headers = response
.headers()
.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect();
let body = response.json().await?;
Ok(OmniResponse {
status,
headers,
body,
})
}
# Dockerfile
FROM rust:1.75 as builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/api-server /usr/local/bin/
EXPOSE 3000
CMD ["api-server"]
$ docker build -t api-server .
$ docker run -p 3000:3000 api-server
# Docker Compose
version: '3.8'
services:
api:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=sqlite:data.db
volumes:
- ./data:/data
축하합니다! 다음을 구현했습니다:
중급 Rust 개발자로서 필요한 핵심 개념들을 모두 배웠습니다. 이제 실제 프로덕션 프로젝트를 만들 준비가 되었습니다.
다음 단계는 WIA-RUST-ADVANCED입니다. 계속 도전하세요!