9 topic. Stack focus: NestJS + Prisma + Postgres + Redis + BullMQ. Bonus: Rust case study từ sales_agent.
5 · Async Processing (BullMQ)
Nguyên tắc: Đừng block HTTP. User click "Send 10k email" → trả 202 Accepted, push job.
const emailQueue = new Queue('email', { connection: redis });
@Post('campaigns/:id/send')
async sendCampaign(@Param('id') id: string) {
await emailQueue.add('send-batch', { campaignId: id }, {
attempts: 3,
backoff: { type: 'exponential', delay: 5000 }, // 5s, 25s, 125s
});
return { status: 'queued' }; // 202
}
new Worker('email', async (job) => {
await sendEmailBatch(job.data.campaignId);
}, { connection: redis, concurrency: 10 });
Pitfall: Job không idempotent → retry tạo duplicate. Dedup key. Worker crash giữa chừng → state machine cho long job.
9 · 🦀 Backend in Rust — sales_agent
Real codebase
Talking point: "patterns transfer across languages". Module structure (handlers/repository/models) chạy được trên NestJS, FastAPI, Axum. Stack: Axum + sqlx + Postgres + Redis + JWT.
▶ Module structure (≡ NestJS feature module)
backend/src/
├── auth/ { mod, models, handlers, jwt, middleware }
├── drafts/ { mod, models, handlers, repository, cleanup }
├── leads/ { mod, models, handlers, repository }
├── error.rs ← unified AppError enum
├── db.rs ← PgPool init
└── routes.rs ← wire handlers to paths
→ Map: auth/ = AuthModule · repository.rs = AuthRepository · handlers.rs = AuthController.
▶ JWT extractor (≡ NestJS Guard)
// auth/middleware.rs
#[axum::async_trait]
impl<S> FromRequestParts<S> for AuthUser
where S: Send + Sync + AsRef<AppState> {
type Rejection = AppError;
async fn from_request_parts(parts: &mut Parts, state: &S)
-> Result<Self, Self::Rejection> {
let header = parts.headers.get("Authorization")
.and_then(|v| v.to_str().ok())
.ok_or_else(|| AppError::Unauthorized("Missing auth header".into()))?;
let token = header.strip_prefix("Bearer ")
.ok_or_else(|| AppError::Unauthorized("Invalid format".into()))?;
let claims = decode_token(token, &state.as_ref().jwt_secret)?;
Ok(AuthUser { user_id: claims.sub })
}
}
→ Extractor Axum ≡ Guard+decorator NestJS ≡ Depends() FastAPI. Inject authed user, fail-fast nếu invalid.
▶ Repository + sqlx
// drafts/repository.rs
pub async fn create(pool: &PgPool, user_id: Uuid, source: &DraftSource, ...)
-> Result<DraftContact, AppError>
{
let draft = sqlx::query_as::<_, DraftContact>(r#"
INSERT INTO draft_contacts (user_id, source, extracted_data,
confidence_data, raw_input, telegram_chat_id, expires_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW() + INTERVAL '24 hours')
RETURNING *
"#)
.bind(user_id).bind(source).bind(extracted_data)
.fetch_one(pool).await?;
Ok(draft)
}
→ Y hệt Prisma repository. Compile-time SQL check (sqlx macro) ≡ TS types từ Prisma. Same benefit, khác syntax.
▶ Async cleanup (không cần Redis queue)
// drafts/cleanup.rs — xóa draft hết hạn mỗi giờ
pub fn start_cleanup_task(pool: PgPool) {
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(3600));
loop {
interval.tick().await;
match repository::delete_expired(&pool).await {
Ok(count) if count > 0 => tracing::info!("Deleted {count} drafts"),
Err(e) => tracing::error!("Cleanup error: {e}"),
_ => {}
}
}
});
}
→ Spawn cho periodic cleanup, metric. BullMQ khi cần retry, scale worker, observability. Đừng over-engineer.
▶ Unified error → HTTP mapping
// error.rs
pub enum AppError {
BadRequest(String), // 400
Unauthorized(String), // 401
NotFound(String), // 404
Conflict(String), // 409
ValidationError(String), // 422
RateLimited(String), // 429
InternalError(anyhow::Error), // 500 — hide details
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, code, msg) = match &self { /* enum → tuple */ };
(status, Json(json!({ "error": { "code": code, "message": msg }}))).into_response()
}
}
→ NestJS có HttpException, FastAPI HTTPException. Rust dùng thiserror derive. Handler chỉ ? propagate.
▶ Migration với ENUM + index
-- migrations/002_create_contacts.sql
DO $$ BEGIN
CREATE TYPE contact_source AS ENUM (
'card_scan', 'text_input', 'telegram',
'linkedin_search', 'linkedin_connection', 'manual'
);
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
CREATE TABLE IF NOT EXISTS contacts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source contact_source NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_contacts_full_name_company
ON contacts (full_name, company_name);
→ DO $$ ... duplicate_object = idempotent migration. ENUM rẻ hơn varchar + check constraint.
💡 Pitch sales_agent trong interview
"Em build 1 backend Rust với Axum + sqlx — hệ thống scan name card, parse contact, push lên HubSpot. Pattern giống NestJS: module-per-feature, repository tách khỏi handler, error enum map sang HTTP status qua trait. Rust ép em handle Option/Result tường minh, ít bug null. Em pick Rust vì có job xử ảnh + AI nặng — tokio handle concurrent tốt với footprint nhỏ."