Mục lục
- Mục Tiêu Bài Học
- Workspace Structure Recap
- Step 1: cargo new Workspace Root
- Step 2: crates/shop-common — Skeleton Library
- Step 3: crates/shop-api — Binary Axum Hello World
- Step 4: .env + dotenvy Cho Config Dev
- Step 5: .gitignore Chuẩn Cho Rust Project
- Step 6: Verify Workspace Chạy
- Step 7: Tree Structure Sau B10
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Có workspace
shop/skeleton đầy đủ chạy đượccargo run -p shop-apitrả response trênhttp://localhost:3000. - Hiểu cấu trúc Cargo workspace với root
Cargo.toml,[workspace],members,[workspace.package]inherit, và[workspace.dependencies]share version cross-crate. - Biết pin Rust toolchain qua file
rust-toolchain.tomlcho reproducible build trên mọi máy dev và CI. - Cấu hình
.envkết hợp crate dotenvy cho dev environment, biết tại sao production KHÔNG dùng file.envmà set env variable qua orchestrator. - Có
.gitignoređúng cho Rust workspace với quyết định commitCargo.lockcho binary app (theo Cargo convention). - Hai crate đầu hoạt động:
shop-api(binary axum hello world) vàshop-common(lib stub gồm 4 moduleconfig,error,telemetry,headers). - Sẵn sàng cho Group 2 Axum Overview ở B11 — đi sâu triết lý axum trên tower và compare với actix-web/rocket.
Workspace Structure Recap
Project Spec đã lock 7 crate cho Shop API khi hoàn thành 316 bài: shop-api (binary HTTP server axum), shop-core (lib domain logic), shop-db (lib PostgreSQL adapter sqlx), shop-cache (lib Redis adapter), shop-worker (binary background job apalis), shop-cli (binary admin CLI), shop-common (lib shared utility). Dependency graph: shop-api phụ thuộc shop-core + shop-db + shop-cache + shop-common; shop-core không phụ thuộc crate nội bộ nào (pure domain); shop-db và shop-cache phụ thuộc shop-core để implement trait abstract.
Init đủ 7 crate ngay từ B10 sẽ overwhelming cho người mới — bạn nhìn 7 thư mục rỗng không biết bắt đầu từ đâu, và 5 crate trong số đó chưa có dependency lock được (sqlx version chốt ở G6, redis crate chốt ở G18, apalis chốt ở G21). Pattern start small, grow incremental của Luca Palmieri trong "Zero To Production in Rust" gợi ý: chỉ init crate tối thiểu để chạy được rồi extend dần khi feature kế tiếp đòi.
Áp dụng cho Shop API: B10 init 2 crate:
shop-api(binary) — axum hello world để verify workspace chạy, đặt nền cho Group 2 Axum Overview.shop-common(lib) — stubconfig,error,telemetry,headersmà mọi crate tương lai cần import (config đọc env, error type chung, header constant lock từ B4).
5 crate còn lại init theo lịch khi feature đến:
shop-coreở Group 4 (B31-B40 Extractors) — bắt đầu domain entity và service trait.shop-dbở Group 6 (B51-B60 PostgreSQL sqlx) — repository impl PostgreSQL.shop-cacheở Group 18 (Redis cache) — session + rate limit + idempotency store.shop-workerở Group 21 (Background job) — apalis runner email + inventory + analytics.shop-cliở Group 29 (Operations) — admin CLI seed + user-create.
Cây thư mục target sau B10:
shop/
├── Cargo.toml # workspace root
├── Cargo.lock # commit (binary app)
├── rust-toolchain.toml # pin Rust 1.85
├── .env.example # commit (schema only)
├── .env # gitignored (giá trị thật)
├── .gitignore
├── README.md
└── crates/
├── shop-api/ # binary axum
│ ├── Cargo.toml
│ └── src/main.rs
└── shop-common/ # lib stub
├── Cargo.toml
└── src/
├── lib.rs
├── config.rs
├── error.rs
├── telemetry.rs
└── headers.rs
Triết lý start small, grow incremental giúp bạn không sa đà quy hoạch trước cấu trúc mà sau này phải refactor — mỗi crate sinh ra khi có lý do cụ thể (đã có code muốn tách module để giảm compile time hoặc share cross-binary), không sinh ra cho có.
Step 1: cargo new Workspace Root
Tạo thư mục root, init git, viết file Cargo.toml workspace bằng tay (không dùng cargo new --workspace vì flag này tạo cấu trúc khác kỳ vọng):
mkdir shop && cd shop
git init
# tạo các thư mục con
mkdir -p crates/shop-api/src crates/shop-common/src
File Cargo.toml workspace root:
# File: shop/Cargo.toml
[workspace]
resolver = "3"
members = [
"crates/shop-api",
"crates/shop-common",
]
[workspace.package]
edition = "2024"
rust-version = "1.85"
authors = ["Shop API Team <[email protected]>"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/example/shop"
[workspace.dependencies]
# axum + tokio stack
axum = "0.8"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["trace", "cors", "compression-gzip"] }
# serde + json
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# error + tracing
anyhow = "1"
thiserror = "2"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
# config + env
dotenvy = "0.15"
# crate nội bộ
shop-common = { path = "crates/shop-common" }
[profile.release]
lto = "thin"
codegen-units = 1
strip = true
Vài điểm cần chú ý:
resolver = "3"— Cargo feature resolver mới hơn (mặc định của edition 2024). Resolver v3 giải quyết một số edge case về feature unification trong workspace mà v2 vẫn còn — đặc biệt khi crate xuất hiện đồng thời như normal dependency vàbuild-dependenciesvới feature khác nhau.[workspace.package]— field share cho mọi member crate. Mỗi crate trongcrates/*chỉ cần khai báoedition.workspace = true,rust-version.workspace = true, ... — không phải lặp lại edition trong từngCargo.toml.[workspace.dependencies]— lock version dependencies chung. Khi nhiều crate cùng dùngserde, nếu mỗi crate khai báo riêngserde = "1.0"rồiserde = "1.0.150"sẽ rất dễ drift version. Workspace.dependencies lock một version, các crate reference quaserde.workspace = truetrong[dependencies]của crate.[profile.release]—lto = "thin"(link-time optimization mode thin nhanh hơn full vẫn giảm binary size đáng kể),codegen-units = 1(tăng tối ưu giảm compile parallelism — chấp nhận build release chậm hơn),strip = true(strip debug symbol khỏi binary production).
File rust-toolchain.toml pin version Rust cho mọi máy dev và CI:
# File: shop/rust-toolchain.toml
[toolchain]
channel = "1.85"
components = ["rustfmt", "clippy"]
profile = "minimal"
Khi developer mới clone repo và gõ cargo build, rustup tự download Rust 1.85 (kèm rustfmt và clippy) — không cần README dài giải thích version nào. CI cũng đọc file này, đảm bảo reproducible build cross-environment. Khi muốn nâng Rust version, chỉ sửa một dòng — toàn team và CI sync ngay PR sau.
Step 2: crates/shop-common — Skeleton Library
shop-common là library crate chứa primitive mà mọi crate khác cần: type AppConfig đọc từ env, type AppError + AppResult<T> chung cho mọi error path, function init_tracing setup tracing subscriber, và constant 4 custom header (đã lock B4). Init shop-common trước shop-api vì shop-api sẽ import shop-common ngay ở main.rs.
Cargo.toml của shop-common:
# File: shop/crates/shop-common/Cargo.toml
[package]
name = "shop-common"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
serde.workspace = true
thiserror.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
anyhow.workspace = true
dotenvy.workspace = true
src/lib.rs re-export 4 module public:
// File: shop/crates/shop-common/src/lib.rs
//! shop-common — shared utility cho mọi crate trong workspace Shop API.
pub mod config;
pub mod error;
pub mod headers;
pub mod telemetry;
src/config.rs — AppConfig struct đọc từ env variable. Field theo Shop API Project Spec: port, database_url, redis_url, jwt_secret, app_env:
// File: shop/crates/shop-common/src/config.rs
//! Application configuration loaded from environment.
use std::env;
use std::str::FromStr;
use crate::error::{AppError, AppResult};
/// Environment hiện tại — phân biệt cho log format, panic handler, swagger-ui enable.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppEnv {
Dev,
Staging,
Production,
}
impl FromStr for AppEnv {
type Err = AppError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"dev" | "development" => Ok(Self::Dev),
"staging" | "stage" => Ok(Self::Staging),
"prod" | "production" => Ok(Self::Production),
other => Err(AppError::BadRequest(format!("unknown APP_ENV: {other}"))),
}
}
}
#[derive(Debug, Clone)]
pub struct AppConfig {
pub app_env: AppEnv,
pub port: u16,
pub database_url: String,
pub redis_url: String,
pub jwt_secret: String,
}
impl AppConfig {
/// Đọc config từ env variable. Gọi `dotenvy::dotenv()` trước để load `.env` (dev).
/// Production KHÔNG có file `.env` — env variable set qua orchestrator.
pub fn from_env() -> AppResult<Self> {
// Im lặng nếu không có `.env` — production case.
dotenvy::dotenv().ok();
let app_env = read_env("APP_ENV")
.unwrap_or_else(|_| "dev".to_string())
.parse::<AppEnv>()?;
let port = read_env("PORT")
.unwrap_or_else(|_| "3000".to_string())
.parse::<u16>()
.map_err(|e| AppError::BadRequest(format!("PORT invalid: {e}")))?;
let database_url = read_env("DATABASE_URL")?;
let redis_url = read_env("REDIS_URL")?;
let jwt_secret = read_env("JWT_SECRET")?;
Ok(Self { app_env, port, database_url, redis_url, jwt_secret })
}
}
fn read_env(key: &str) -> AppResult<String> {
env::var(key).map_err(|_| AppError::BadRequest(format!("missing env: {key}")))
}
src/error.rs — AppError enum 11 variant theo mapping HTTP status đã lock từ B3:
// File: shop/crates/shop-common/src/error.rs
//! AppError — type error duy nhất qua mọi crate, map sang HTTP status ở B16 (G2).
//! Mapping HTTP status đã lock từ B3.
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
/// 400 — parse fail (JSON syntax sai, body trống, query param invalid).
#[error("bad request: {0}")]
BadRequest(String),
/// 401 — chưa cung cấp token, token sai/expired. Response kèm `WWW-Authenticate: Bearer`.
#[error("unauthenticated")]
Unauthenticated,
/// 403 — đã auth nhưng không có quyền truy cập resource.
#[error("forbidden")]
Forbidden,
/// 404 — resource không tồn tại. Có thể dùng giấu 403 cho privacy.
#[error("not found")]
NotFound,
/// 405 — method không hỗ trợ. Response kèm `Allow` header.
#[error("method not allowed")]
MethodNotAllowed,
/// 409 — conflict: duplicate email, optimistic-lock version mismatch.
#[error("conflict: {0}")]
Conflict(String),
/// 422 — parse OK nhưng business rule fail. Body kèm list field errors.
#[error("validation failed: {0}")]
Validation(String),
/// 429 — rate limit. Response kèm `Retry-After`.
#[error("rate limited")]
RateLimited,
/// 500 — panic, uncaught error. KHÔNG expose stack trace ra client.
#[error("internal error")]
Internal(#[from] anyhow::Error),
/// 502/504 — upstream invalid response hoặc timeout.
#[error("upstream error: {0}")]
Upstream(String),
/// 503 — overload, maintenance. Response kèm `Retry-After`.
#[error("service unavailable")]
Unavailable,
}
/// Alias dùng xuyên suốt mọi crate cho hàm trả `Result<T, AppError>`.
pub type AppResult<T> = Result<T, AppError>;
src/telemetry.rs — function init_tracing stub setup tracing subscriber, JSON format cho production, pretty format cho dev. Chi tiết full observability (OpenTelemetry, Jaeger, Prometheus) ở Group 22:
// File: shop/crates/shop-common/src/telemetry.rs
//! Tracing init stub. Chi tiết observability đầy đủ ở Group 22.
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
/// Init tracing subscriber với env filter từ `RUST_LOG`.
/// Dev: pretty format readable; Production: JSON cho log aggregator.
pub fn init_tracing(json: bool) {
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"));
let registry = tracing_subscriber::registry().with(filter);
if json {
registry.with(fmt::layer().json()).init();
} else {
registry.with(fmt::layer().pretty()).init();
}
}
src/headers.rs — constant 4 custom header lock từ B4 (note section "Custom Headers" trong shop-state.md):
// File: shop/crates/shop-common/src/headers.rs
//! Custom HTTP header constant — lock từ B4 cho toàn series Shop API.
//! Mọi crate import từ đây, KHÔNG tự viết string header thẳng trong code.
/// UUID v4 do edge middleware sinh, propagate vào tracing span + error envelope.
/// Client tự gửi sẽ được tôn trọng (use case API gateway có request_id riêng).
pub const X_REQUEST_ID: &str = "x-request-id";
/// KHÔNG prefix `X-` theo convention Stripe. Client gửi trên POST mutate để
/// server dedupe khi retry — TTL 24h qua Redis `idem:<key>`.
pub const IDEMPOTENCY_KEY: &str = "idempotency-key";
/// Response header rate-limit (G17 Redis token bucket / sliding window).
pub const X_RATE_LIMIT_LIMIT: &str = "x-ratelimit-limit";
pub const X_RATE_LIMIT_REMAINING: &str = "x-ratelimit-remaining";
pub const X_RATE_LIMIT_RESET: &str = "x-ratelimit-reset";
/// Tổng số record cho list endpoint phân trang (G7 B64).
/// Áp dụng mọi list: products, orders, reviews, admin lists.
pub const X_TOTAL_COUNT: &str = "x-total-count";
Tên header dùng lowercase theo convention HTTP/2 (RFC 7540) và http::HeaderName trong axum — case-insensitive nhưng lowercase là canonical form. Khi build response, dùng headers::X_REQUEST_ID trực tiếp không phải viết "x-request-id" string mỗi nơi.
Step 3: crates/shop-api — Binary Axum Hello World
Crate shop-api là binary chính của server. Hello world chỉ có hai route / trả version string và /health trả ok để verify pipeline chạy đầu cuối từ Cargo build đến HTTP request — chưa tích hợp database, chưa có auth, chưa có middleware. Mọi feature gradually thêm vào ở các bài kế.
Cargo.toml của shop-api:
# File: shop/crates/shop-api/Cargo.toml
[package]
name = "shop-api"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
[[bin]]
name = "shop-api"
path = "src/main.rs"
[dependencies]
# crate nội bộ
shop-common.workspace = true
# axum stack
axum.workspace = true
tokio.workspace = true
tower.workspace = true
tower-http.workspace = true
# serde
serde.workspace = true
serde_json.workspace = true
# observability + error
tracing.workspace = true
tracing-subscriber.workspace = true
anyhow.workspace = true
Section [[bin]] khai báo explicit binary target — không bắt buộc khi chỉ có một binary tên trùng package, nhưng giữ rõ ràng để sau này dễ thêm binary phụ (vd src/bin/export_openapi.rs đã lock cho B8 sinh spec JSON ra CI artifact).
src/main.rs — entry point binary:
// File: shop/crates/shop-api/src/main.rs
//! Shop API binary — HTTP server axum trên tokio runtime.
use axum::{Router, routing::get};
use shop_common::config::{AppConfig, AppEnv};
use shop_common::telemetry;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = AppConfig::from_env()?;
// JSON log cho production, pretty cho dev/staging readable.
telemetry::init_tracing(config.app_env == AppEnv::Production);
let app = build_router();
let addr = ("0.0.0.0", config.port);
let listener = tokio::net::TcpListener::bind(addr).await?;
tracing::info!(
port = config.port,
env = ?config.app_env,
"shop-api listening"
);
axum::serve(listener, app).await?;
Ok(())
}
/// Build router gốc. Sẽ extend với module routes ở B11+ Group 2.
fn build_router() -> Router {
Router::new()
.route("/", get(root))
.route("/health", get(health))
}
async fn root() -> &'static str {
"shop-api v0.1.0"
}
async fn health() -> &'static str {
"ok"
}
Cấu trúc minimal nhưng đặt nền cho mọi feature tới: build_router() hàm riêng để test sau (axum-test sẽ chạy router trực tiếp không cần bind port); tokio::main macro multi-thread runtime mặc định; anyhow::Result ở main để error message friendly khi server crash startup; tracing::info! structured log đầu tiên — output format theo app_env đã chọn.
Step 4: .env + dotenvy Cho Config Dev
AppConfig::from_env() đã gọi dotenvy::dotenv().ok() để load file .env từ cwd vào process environment trước khi env::var đọc. Pattern này chuẩn cho dev workflow: bạn không phải export DATABASE_URL=... mỗi shell mới, chỉ cần cp .env.example .env rồi cargo run -p shop-api.
File .env.example commit vào git (schema cho dev biết cần set gì):
# File: shop/.env.example
# Copy thành `.env` rồi điền giá trị cụ thể.
# `.env` đã được gitignore.
APP_ENV=dev
PORT=3000
# PostgreSQL — local docker-compose hoặc instance dev riêng (G6 sẽ setup).
DATABASE_URL=postgres://shop:shop@localhost:5432/shop_dev
# Redis — local docker (G18 sẽ setup).
REDIS_URL=redis://localhost:6379
# JWT signing key — production rotate qua secret manager.
JWT_SECRET=change-me-32-bytes-minimum-for-hs256
# Log filter (RUST_LOG syntax: target=level,target=level)
RUST_LOG=info,shop_api=debug,sqlx=warn
File .env thật (cùng schema, giá trị cụ thể dev local):
# File: shop/.env
# KHÔNG commit — gitignore.
APP_ENV=dev
PORT=3000
DATABASE_URL=postgres://shop:shop@localhost:5432/shop_dev
REDIS_URL=redis://localhost:6379
JWT_SECRET=dev-only-secret-not-for-production-use
RUST_LOG=info,shop_api=debug,sqlx=warn
Production KHÔNG dùng file .env. Env variable được set qua orchestrator: Docker (--env-file chỉ vào path runtime mount từ secret), Kubernetes (envFrom: secretRef mount từ Secret), fly.io (fly secrets set), Railway (dashboard hoặc CLI), systemd (EnvironmentFile=). Lý do:
- Audit trail: secret manager log mỗi lần đọc/set, file
.envnằm yên trong filesystem không có audit. - Rotation: secret manager hỗ trợ rotate transparent (dùng version label),
.envphải SSH lên server sửa tay. - Encryption at rest: secret manager mã hóa storage,
.envplaintext. - Access control: secret manager IAM permission rõ ràng,
.envai có shell access đều đọc được.
dotenvy::dotenv().ok() trả Result, gọi .ok() để chuyển thành Option và bỏ qua — production không có file là expected, không phải error. Sau khi load (hoặc không load được), env::var đọc bình thường: dev đọc từ .env, production đọc từ env variable đã set sẵn.
Crate dotenvy là fork active của dotenv (crate gốc đã unmaintained từ 2020). API tương thích — code dotenv cũ chỉ cần thay tên import.
Step 5: .gitignore Chuẩn Cho Rust Project
File .gitignore chuẩn cho Rust workspace + dev tooling:
# File: shop/.gitignore
# Rust build artifacts
/target/
# Env files chứa secret (commit `.env.example` ở trên thay schema)
/.env
/.env.local
/.env.*.local
# IDE artifacts
/.idea/
/.vscode/*
!/.vscode/settings.json
!/.vscode/extensions.json
# OS
.DS_Store
Thumbs.db
# Logs
*.log
# Node (nếu sau có frontend hoặc tooling JS như Newman)
/node_modules/
/dist/
# Coverage report
/tarpaulin-report.html
/cobertura.xml
# sqlx offline mode cache — sẽ generate ở G6, để bản generated trong git
# .sqlx/ (uncomment khi G6 setup offline mode)
Vài quyết định cần giải thích:
Cargo.lockkhông xuất hiện trong.gitignore— Shop API là binary application, theo Cargo FAQ: commitCargo.lockcho binary app, KHÔNG commit cho library crate publish lên crates.io. Lý do: reproducible build — mọi máy dev và CI build với cùng exact version dependency, tránh case "trên máy mình chạy ổn nhưng staging fail" dotokionhảy minor version. Library crate ngược lại không commit Cargo.lock vì người dùng crate sẽ resolve version chung với dependency khác — lock file của library không có ý nghĩa./target/chứa build artifact và cache compiler — kích thước hàng GB sau vài tuần dev, không commit.cargo cleanxóa được..envvà biến thể local — chứa secret dev (database password, JWT key), tuyệt đối không commit. Pattern.env.*.localtheo convention Vite/dotenv-cli cho dev environment riêng (vd.env.staging.local)..vscode/*với exception!/.vscode/settings.json— ignore mọi file VS Code workspace trừ settings shared (rust-analyzer config, format-on-save). Pattern phẩy âm!override rule trước nó.- node_modules — chuẩn bị cho khi cần Newman CI (B9 đã quyết định smoke test qua Newman) hoặc frontend tooling.
Sau B10 chạy cargo build sẽ sinh Cargo.lock ở root. Mọi crate member dùng chung Cargo.lock này — không sinh riêng Cargo.lock trong crates/shop-api/ hay crates/shop-common/.
Step 6: Verify Workspace Chạy
Trước khi run, copy .env.example thành .env (file thật cho dev local):
cp .env.example .env
# Sửa lại JWT_SECRET nếu muốn
Build cả workspace:
cd shop
cargo build
# Compiling shop-common v0.1.0 (crates/shop-common)
# Compiling shop-api v0.1.0 (crates/shop-api)
# Finished `dev` profile [unoptimized + debuginfo] target(s) in ~30s
Build lần đầu mất 30-60 giây vì compile axum + tokio + dependencies tree. Build incremental kế (chỉ sửa file main.rs) chỉ vài giây.
Run binary:
cargo run -p shop-api
# Compiling shop-api v0.1.0 ...
# Finished `dev` profile in 1.2s
# Running `target/debug/shop-api`
# 2026-06-12T07:30:15.123Z INFO shop_api: shop-api listening port=3000 env=Dev
Flag -p shop-api nói Cargo chạy package tên shop-api — quan trọng trong workspace nhiều binary để Cargo không nhầm. Không có -p Cargo sẽ báo error "could not determine which binary to run" khi sau này có thêm shop-worker và shop-cli.
Mở shell mới test với curl (workflow đã lock ở B9):
# Health check
curl http://localhost:3000/health
# ok
# Root
curl -v http://localhost:3000/
# * Trying 127.0.0.1:3000...
# * Connected to localhost (127.0.0.1) port 3000
# > GET / HTTP/1.1
# > Host: localhost:3000
# > User-Agent: curl/8.6.0
# > Accept: */*
# < HTTP/1.1 200 OK
# < content-type: text/plain; charset=utf-8
# < content-length: 15
# < date: Fri, 12 Jun 2026 07:30:42 GMT
# shop-api v0.1.0
Verify pipeline hoàn chỉnh: Cargo build → binary chạy → tokio runtime accept TCP → axum router match path → handler trả response → curl in body. Bất kỳ bước nào fail (vd port 3000 đã bị process khác chiếm) cần fix trước khi sang B11.
Suggested commit cho B10:
git add .
git commit -m "B10: init workspace shop với shop-api + shop-common skeleton
- Cargo workspace root, resolver 3, edition 2024, Rust 1.85 pinned
- shop-api binary axum hello world, /health + /
- shop-common lib: config (AppConfig::from_env), error (AppError 11),
telemetry (init_tracing), headers (4 custom header B4 lock)
- .env.example + dotenvy cho dev; .gitignore commit Cargo.lock
- Ready cho G2 Axum Overview ở B11"
Step 7: Tree Structure Sau B10
Cây thư mục state sau khi hoàn thành B10:
shop/
├── Cargo.toml # workspace root (resolver 3, members, workspace.deps)
├── Cargo.lock # generated bởi `cargo build`, commit vào git
├── rust-toolchain.toml # pin Rust 1.85 + rustfmt + clippy
├── .env.example # schema commit vào git
├── .env # giá trị thật, gitignored
├── .gitignore
├── README.md # placeholder, content sau
└── crates/
├── shop-api/
│ ├── Cargo.toml # [[bin]] shop-api, deps axum+tokio+shop-common
│ └── src/
│ └── main.rs # tokio::main, Router /, /health
└── shop-common/
├── Cargo.toml # lib, deps serde+thiserror+tracing+dotenvy
└── src/
├── lib.rs # re-export 4 module
├── config.rs # AppConfig, AppEnv, from_env
├── error.rs # AppError (11 variants), AppResult<T>
├── telemetry.rs # init_tracing stub
└── headers.rs # X_REQUEST_ID, IDEMPOTENCY_KEY, X_RATELIMIT_*, X_TOTAL_COUNT
Tổng số file source: 7 (2 file Cargo.toml crate, 1 file Cargo.toml root, 1 file main.rs, 5 file shop-common/src/*.rs). Kết hợp các file config (rust-toolchain.toml, .env.example, .gitignore, README.md) — repo có ~12 file source.
Roadmap thêm crate cho các Group sau:
- G4 (B31-B40) — thêm
crates/shop-core/khi bắt đầu domain entity + service trait.shop-corekhông phụ thuộc crate nào nội bộ (pure domain),shop-apisẽ addshop-core.workspace = true. - G6 (B51-B60) — thêm
crates/shop-db/khi setup sqlx PostgreSQL. Crate này impl trait từshop-core::repository. Workspace.dependencies thêmsqlx. - G18 — thêm
crates/shop-cache/khi setup Redis. Workspace.dependencies thêmfredhoặcredis. - G21 — thêm
crates/shop-worker/binary khi setup apalis background job. Workspace.dependencies thêmapalis. - G29 — thêm
crates/shop-cli/binary cho admin command. Workspace.dependencies thêmclap.
Mỗi lần thêm crate chỉ cần: (1) tạo folder + file Cargo.toml và src; (2) thêm vào members của workspace root Cargo.toml; (3) optionally thêm vào workspace.dependencies nếu sẽ được crate khác import. Cargo workspace tự discover member và share build cache.
Tổng Kết
- Workspace root
Cargo.tomlvớiresolver = "3"(mặc định edition 2024, fix một số edge case feature unification),memberslist explicit,[workspace.package]share field cross-crate (edition, rust-version, authors, license),[workspace.dependencies]lock một version dùng chung — crate member reference qua.workspace = true. - Hai crate đầu:
shop-api(binary axum hello world chạy port 3000 với/và/health) +shop-common(lib stub 4 module:config,error,telemetry,headers). 5 crate còn lại init dần ở G4 (shop-core), G6 (shop-db), G18 (shop-cache), G21 (shop-worker), G29 (shop-cli) — pattern start small, grow incremental. rust-toolchain.tomlpinchannel = "1.85"vớicomponents = ["rustfmt", "clippy"]— rustup tự install version chính xác khi dev clone repo và build, reproducible cross-machine + CI.AppConfig::from_env()đọc env variable quadotenvy::dotenv().ok()+env::var. Dev load file.env, production set env trực tiếp qua orchestrator (Docker/K8s/fly.io/Railway). 5 field:app_env,port,database_url,redis_url,jwt_secret.AppErrorenum 11 variant theo B3 lock:BadRequest,Unauthenticated,Forbidden,NotFound,MethodNotAllowed,Conflict,Validation,RateLimited,Internal,Upstream,Unavailable.AppResult<T>alias =Result<T, AppError>.shop-common::headersconstant 4 custom header lock từ B4:X_REQUEST_ID(UUID v4 propagate),IDEMPOTENCY_KEY(không prefixX-theo Stripe convention),X_RATE_LIMIT_*(3 header response report quota),X_TOTAL_COUNT(list endpoint pagination).- Cargo.lock commit cho Shop API vì là binary application (reproducible build). Library crate publish lên crates.io ngược lại KHÔNG commit Cargo.lock.
cargo run -p shop-api→ server localhost:3000 lên với/healthtrảokvà/trảshop-api v0.1.0. Verify curl pipeline đầu cuối thành công. Suggested commit "B10: init workspace shop với shop-api + shop-common skeleton".
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Workspace root
Cargo.tomlkhai báoresolver = "3"nghĩa là gì? Khácresolver = "2"ra sao? Khi nào edge case của resolver mới giải quyết được mà v2 không? - Tại sao Shop API commit
Cargo.lockvào git? Library crate publish lên crates.io có commit Cargo.lock không, vì sao? [workspace.dependencies]trong rootCargo.tomlđể làm gì? Khi nhiều crate cùng dùngserde, dùngworkspace.dependenciesmang lợi ích cụ thể nào so với mỗi crate khai báoserde = "1"riêng?- File
.envvà.env.example: cái nào commit vào git, cái nào gitignore? Tại sao production KHÔNG dùng file.envdù backend code vẫn gọidotenvy::dotenv().ok()? - Muốn thêm crate thứ ba
shop-corevào workspace (ở G4 sẽ làm), bạn cần thay đổi gì trong workspace rootCargo.tomlvà tạo những file nào trongcrates/shop-core/? Mô tả 3-4 bước cụ thể.
Đáp án
resolver = "3"là Cargo feature resolver version 3, mặc định khi crate dùng edition 2024. Resolver quyết định cách Cargo "unify" feature giữa các phiên bản của cùng một crate xuất hiện ở nhiều vị trí trong dependency graph. Khác chính so với v2: v3 giải quyết tốt hơn edge case khi cùng một crate xuất hiện đồng thời như normal dependency và build-dependencies với feature flags khác nhau — v2 unify mạnh tay làm build-script feature leak ra runtime (vd build-script bật featurestdcủalibclàm runtime no_std crate bị vỡ); v3 tách context kỹ hơn để mỗi vị trí giữ feature riêng. Edge case khác làresolver = "3"respectrust-versionfield khi pick version mới — tránh chọn dependency yêu cầu Rust mới hơn version đang dùng. Workspace mới luôn nên dùng resolver mới nhất.- Shop API commit
Cargo.lockvì là binary application deploy vào production. Lý do: reproducible build — mọi máy dev và CI build cùng exact version dependency tree (cả transitive dependency), tránh case "build pass trên máy dev nhưng staging fail" dotokionhảy minor version (vd1.40.0→1.41.0) khi không có lock file. Lock file cũng giúpcargo auditscan vulnerability chính xác. Library crate publish lên crates.io KHÔNG commit Cargo.lock vì người dùng crate cài về sẽ resolve dependency chung với crate khác trong project của họ — lock file của library bị ignore khi cài làm dep, chỉ có ý nghĩa nếu chạy test/example riêng. Theo Cargo FAQ: app commit, lib không.shop-commontrong workspace tuy là lib nhưng KHÔNG publish lên crates.io (chỉ dùng nội bộ quapath), nên Cargo.lock chung của workspace vẫn commit — quyết định ở mức workspace, không mức crate. [workspace.dependencies]là nơi khai báo shared dependency declaration dùng chung qua các crate trong workspace. Mỗi crate member chỉ cần viếtserde.workspace = truetrong[dependencies]của crate, không phải lặp version + features mỗi nơi. Lợi ích cụ thể khi nhiều crate cùng dùngserde: (1) Version single-source-of-truth — nângserdetừ1.0.150lên1.0.200chỉ sửa một dòng ở workspace root, mọi crate sync ngay. Nếu khai báo riêng dễ drift (crate A đang1.0.150, crate B đang1.0.200, build error khi macro derive khác nhau). (2) Feature unified explicit — workspace level định feature list (vdfeatures = ["derive"]), không bị case crate A bậtderive, crate B không bật → Cargo unify dùng union nhưng đôi khi build cache invalidate liên tục. (3) Tránh duplicate compilation — Cargo build chia sẻ cùng version compile một lần dùng nhiều nơi (cùng resolver chính xác hơn). (4) Audit + dependency review nhanh — tìm versionserdetrong project mở một file workspace Cargo.toml thay vì grep N file..env.examplecommit vào git làm schema — list tên env variable cần set với placeholder (JWT_SECRET=change-me-32-bytes-minimum), comment giải thích. Mọi dev clone repo biết ngay cần config gì..envgitignore — chứa giá trị thật, kể cả dev local cũng có password database, JWT secret — không thuộc về code repository. Production KHÔNG dùng file.envvì: (a) audit trail — secret manager (Vault, AWS Secrets Manager, GitHub Actions secrets) log mỗi lần đọc/set; (b) rotation transparent qua version label; (c) encryption at rest mặc định; (d) access control IAM per-secret. Backend code vẫn gọidotenvy::dotenv().ok()— production không có file.envnêndotenv()trảErr,.ok()chuyển thànhNonebỏ qua. Sau đóenv::var("DATABASE_URL")đọc bình thường — production lấy từ env variable mà orchestrator đã inject vào process trước khi start binary.- Thêm crate
shop-core: Bước 1: tạo thư mục và file source:mkdir -p crates/shop-core/src, tạocrates/shop-core/Cargo.tomlvới[package] name = "shop-core" version = "0.1.0"+ inherit field workspace + dependencies cần (serde, thiserror, anyhow, có thể shop-common nếu reuse error/header), tạocrates/shop-core/src/lib.rsban đầu chỉ cần một dòng comment. Bước 2: thêm path vàomemberscủa workspace rootCargo.toml:members = ["crates/shop-api", "crates/shop-common", "crates/shop-core"]. Bước 3: thêm vào[workspace.dependencies]để crate khác import được:shop-core = { path = "crates/shop-core" }. Bước 4: ở crate cần import (vdshop-api), thêmshop-core.workspace = truetrong[dependencies]. Chạycargo buildverify workspace pickup crate mới — output sẽ thấyCompiling shop-core v0.1.0. Pattern này lặp y nguyên cho mọi crate thêm sau (shop-dbở G6,shop-cacheở G18, ...).
Bài Tiếp Theo
Bài 11: Axum Là Gì? Sinh Ra Từ Đâu — bắt đầu Group 2 Axum Overview: triết lý axum (tower-based, type-safe extractor system, KHÔNG dùng macro magic) ra mắt 2021 từ team tokio-rs, so với actix-web (actor model + custom executor) và rocket (macro-heavy stateful), case adoption thực tế Cloudflare Workers Rust + AWS Labs + Shuttle, lý do Shop API chọn axum cho Group 1-31 (ecosystem tower share với tonic gRPC ở B314 + tốc độ compile + integration với hyper 1).
