Introduction: Why Rust for Microservices?
Microservice architecture has become the standard for modern distributed systems. However, choosing a programming language for implementing microservices is a critical decision. Rust offers a unique combination of C/C++ performance and memory safety, making it an ideal candidate for high-load services.
In this article, we will explore how to build a microservice in Rust using the popular frameworks Actix-web and Tokio. You will learn about architectural principles, asynchronous request processing, and get working code examples.
Microservice Architecture in Rust
Basic Principles
A microservice in Rust is typically built around the following components:
- HTTP server — handling incoming requests (Actix-web, Warp, Axum).
- Async runtime — managing concurrency (Tokio, async-std).
- Business logic — isolated modules with clear boundaries.
- Data storage — interacting with databases via SQLx or Diesel.
- Inter-service communication — HTTP/gRPC or message queues (RabbitMQ, Kafka).
Project Structure
Recommended structure for a microservice in Rust:
my_microservice/├── Cargo.toml├── src/│ ├── main.rs # Entry point, server startup│ ├── routes/ # Route definitions│ ├── handlers/ # Request handlers│ ├── models/ # Data structures (DTOs, entities)│ ├── services/ # Business logic│ ├── repositories/ # Data access layer│ └── config/ # Configuration (env, files)└── tests/ # Integration testsPractical Example: URL Shortener Service
Let's look at creating a simple microservice for URL shortening. It will accept long URLs and return short identifiers.
Step 1: Project Setup
Create a new Cargo project and add dependencies to Cargo.toml:
[package]name = "url-shortener"version = "0.1.0"edition = "2021"
[dependencies]actix-web = "4"actix-rt = "2"serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }redis = { version = "0.23", features = ["tokio-comp"] }uuid = { version = "1", features = ["v4"] }dotenv = "0.15"env_logger = "0.10"log = "0.4"Step 2: Data Models
Let's define the request and response structures:
// models/mod.rsuse serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]pub struct CreateUrlRequest { pub url: String,}
#[derive(Debug, Serialize)]pub struct CreateUrlResponse { pub short_code: String, pub original_url: String,}
#[derive(Debug, Serialize)]pub struct ErrorResponse { pub error: String,}Step 3: Service Layer
Let's implement the business logic using Redis for storage:
// services/url_service.rsuse redis::AsyncCommands;use uuid::Uuid;
pub struct UrlService { redis_client: redis::Client,}
impl UrlService { pub fn new(redis_client: redis::Client) -> Self { Self { redis_client } }
pub async fn create_short_url(&self, original_url: &str) -> Result<String, String> { let short_code = Uuid::new_v4().to_string()[..8].to_string(); let mut conn = self.redis_client .get_multiplexed_async_connection() .await .map_err(|e| format!("Redis connection error: {}", e))?;
// Save to Redis: key — short_code, value — original_url conn.set_ex(&short_code, original_url, 86400) // TTL 24 hours .await .map_err(|e| format!("Redis