Database Integration with Hyperlane
Project Code:https://github.com/hyperlane-dev/hyperlane
Introduction
Hyperlane is a lightweight, high-performance, cross-platform Rust HTTP server library built on top of Tokio. While hyperlane itself focuses on HTTP handling and middleware, real-world applications almost always need to interact with databases. This article explores how to integrate MySQL, PostgreSQL, and Redis into your hyperlane-based applications, covering connection pool configuration, best practices, and practical code examples.
Table of Contents
- Overview of Database Integration
- MySQL Integration
- PostgreSQL Integration
- Redis Integration
- Connection Pool Configuration
- Middleware-Based Database Access
- Error Handling for Database Operations
- Performance Considerations
- Conclusion
Overview of Database Integration
Hyperlane provides a flexible middleware system that makes it straightforward to integrate database connections into the request lifecycle. The key idea is to initialize database connection pools during server startup, store them in the application context, and access them through middleware or route handlers.
Hyperlane's ecosystem includes several tools and plugins that complement database integration:
- hyperlane-utils — Utility functions for hyperlane applications
- hyperlane-log — Structured logging for hyperlane
- hyperlane-time — Time utilities
- utoipa — API documentation generation
- server-manager — Server lifecycle management
- hyperlane-broadcast — Broadcasting capabilities
- hyperlane-plugin-websocket — WebSocket plugin
Combined with Rust's excellent database libraries, you can build robust, type-safe database-driven applications.
MySQL Integration
Setting Up the Connection Pool
To integrate MySQL with hyperlane, you can use the sqlx crate, which provides async database access with built-in connection pooling. Initialize the pool during server startup and pass it to your route handlers.
use hyperlane::*;
use sqlx::mysql::MySqlPoolOptions;
use sqlx::MySqlPool;
#[tokio::main]
async fn main() {
let pool = MySqlPoolOptions::new()
.max_connections(20)
.min_connections(5)
.acquire_timeout(std::time::Duration::from_secs(5))
.connect("mysql://user:password@localhost:3306/mydb")
.await
.expect("Failed to create MySQL pool");
let server = Server::default();
// Store pool and configure routes...
server.run().await;
}
Using MySQL in Route Handlers
With hyperlane's #[route] attribute and request attributes, you can cleanly handle database operations:
use hyperlane::*;
use sqlx::MySqlPool;
#[route("/users")]
async fn get_users(ctx: Context) -> Result<(), RequestError> {
let pool = ctx.get_request().get_body_string();
// Query using sqlx with the pool
let users = sqlx::query_as::<_, User>("SELECT id, name, email FROM users")
.fetch_all(&pool)
.await
.map_err(|e| RequestError::new(&format!("Database error: {}", e), 500))?;
let mut response = ctx.get_mut_response();
response.set_status_code(200);
response.set_body(serde_json::to_string(&users).unwrap());
Ok(())
}
Fetching Data with Request Attributes
Hyperlane's request attributes make it easy to extract data from incoming requests before performing database operations:
#[route("/users/{id}")]
#[request_path]
async fn get_user_by_id(ctx: Context) -> Result<(), RequestError> {
let user_id = ctx.try_get_route_param("id").unwrap_or_default();
let pool = /* get pool from context */;
let user = sqlx::query_as::<_, User>(
"SELECT id, name, email FROM users WHERE id = ?"
)
.bind(user_id.parse::<i32>().unwrap_or(0))
.fetch_optional(&pool)
.await
.map_err(|e| RequestError::new(&format!("Database error: {}", e), 500))?;
match user {
Some(u) => {
let mut response = ctx.get_mut_response();
response.set_status_code(200);
response.set_body(serde_json::to_string(&u).unwrap());
}
None => {
let mut response = ctx.get_mut_response();
response.set_status_code(404);
response.set_body(r#"{"error": "User not found"}"#);
}
}
Ok(())
}
PostgreSQL Integration
Setting Up the PostgreSQL Pool
PostgreSQL integration follows a similar pattern to MySQL but uses sqlx::postgres instead:
use hyperlane::*;
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
#[tokio::main]
async fn main() {
let pool = PgPoolOptions::new()
.max_connections(30)
.min_connections(5)
.acquire_timeout(std::time::Duration::from_secs(5))
.idle_timeout(std::time::Duration::from_secs(300))
.connect("postgres://user:password@localhost:5432/mydb")
.await
.expect("Failed to create PostgreSQL pool");
let server = Server::default();
server.run().await;
}
Using PostgreSQL in Route Handlers
PostgreSQL queries in hyperlane follow the same pattern as MySQL, with SQL parameter syntax differences:
#[route("/products/{id}")]
#[request_path]
async fn get_product(ctx: Context) -> Result<(), RequestError> {
let product_id = ctx.try_get_route_param("id").unwrap_or_default();
let product = sqlx::query_as::<_, Product>(
"SELECT id, name, price, description FROM products WHERE id = $1"
)
.bind(product_id.parse::<i32>().unwrap_or(0))
.fetch_optional(/* pool */)
.await
.map_err(|e| RequestError::new(&format!("Database error: {}", e), 500))?;
let mut response = ctx.get_mut_response();
match product {
Some(p) => {
response.set_status_code(200);
response.set_body(serde_json::to_string(&p).unwrap());
}
None => {
response.set_status_code(404);
response.set_body(r#"{"error": "Product not found"}"#);
}
}
Ok(())
}
Inserting Data with JSON Body
Hyperlane's #[request_body_json] attribute makes it easy to parse incoming JSON into Rust structs for database insertion:
#[route("/products")]
#[request_body_json]
async fn create_product(ctx: Context) -> Result<(), RequestError> {
let body = ctx.get_request().get_body_string();
// Parse and insert into database
let result = sqlx::query(
"INSERT INTO products (name, price, description) VALUES ($1, $2, $3) RETURNING id"
)
.bind(&product.name)
.bind(product.price)
.bind(&product.description)
.fetch_one(/* pool */)
.await
.map_err(|e| RequestError::new(&format!("Insert error: {}", e), 500))?;
let mut response = ctx.get_mut_response();
response.set_status_code(201);
response.set_body(r#"{"status": "created"}"#);
Ok(())
}
Redis Integration
Setting Up the Redis Connection
Redis is commonly used for caching, session storage, and real-time features. Here's how to set up a Redis connection pool:
use hyperlane::*;
use redis::aio::MultiplexedConnection;
use redis::Client;
#[tokio::main]
async fn main() {
let redis_client = Client::open("redis://127.0.0.1:6379/")
.expect("Failed to create Redis client");
let redis_conn = redis_client.get_multiplexed_async_connection()
.await
.expect("Failed to connect to Redis");
let server = Server::default();
server.run().await;
}
Using Redis for Caching
Redis is frequently used as a caching layer in front of traditional databases. Here's a pattern for caching query results:
#[route("/cached-users/{id}")]
#[request_path]
async fn get_cached_user(ctx: Context) -> Result<(), RequestError> {
let user_id = ctx.try_get_route_param("id").unwrap_or_default();
let cache_key = format!("user:{}", user_id);
// Try to get from Redis cache first
let cached: Option<String> = redis::cmd("GET")
.arg(&cache_key)
.query_async(&mut redis_conn)
.await
.unwrap_or(None);
if let Some(data) = cached {
let mut response = ctx.get_mut_response();
response.set_status_code(200);
response.set_header("X-Cache", "HIT");
response.set_body(data);
return Ok(());
}
// Cache miss — query database
let user = sqlx::query_as::<_, User>(
"SELECT id, name, email FROM users WHERE id = $1"
)
.bind(user_id.parse::<i32>().unwrap_or(0))
.fetch_optional(/* pool */)
.await
.map_err(|e| RequestError::new(&format!("Database error: {}", e), 500))?;
match user {
Some(u) => {
let json = serde_json::to_string(&u).unwrap();
// Store in Redis cache with TTL
let _: () = redis::cmd("SETEX")
.arg(&cache_key)
.arg(3600)
.arg(&json)
.query_async(&mut redis_conn)
.await
.unwrap_or(());
let mut response = ctx.get_mut_response();
response.set_status_code(200);
response.set_header("X-Cache", "MISS");
response.set_body(json);
}
None => {
let mut response = ctx.get_mut_response();
response.set_status_code(404);
response.set_body(r#"{"error": "User not found"}"#);
}
}
Ok(())
}
Using Redis for Session Storage
Redis is an excellent choice for session storage in hyperlane applications:
#[request_middleware(1)]
async fn session_middleware(ctx: Context) -> Result<(), RequestError> {
let cookie_header = ctx.get_request().get_header("cookie").unwrap_or_default();
// Parse session cookie
let session_id = parse_session_cookie(&cookie_header);
if let Some(sid) = session_id {
let session_key = format!("session:{}", sid);
let session_data: Option<String> = redis::cmd("GET")
.arg(&session_key)
.query_async(&mut redis_conn)
.await
.unwrap_or(None);
if let Some(data) = session_data {
// Session found — extend TTL
let _: () = redis::cmd("EXPIRE")
.arg(&session_key)
.arg(7200)
.query_async(&mut redis_conn)
.await
.unwrap_or(());
}
}
Ok(())
}
Connection Pool Configuration
Pool Sizing
Proper connection pool sizing is critical for application performance. Here are key configuration parameters:
// MySQL pool configuration
let mysql_pool = MySqlPoolOptions::new()
.max_connections(50) // Maximum number of connections
.min_connections(10) // Minimum idle connections
.acquire_timeout(Duration::from_secs(5)) // Max wait for a connection
.idle_timeout(Duration::from_secs(600)) // Max idle time before closing
.max_lifetime(Duration::from_secs(1800)) // Max lifetime of a connection
.connect("mysql://user:password@localhost:3306/mydb")
.await
.expect("Failed to create pool");
// PostgreSQL pool configuration
let pg_pool = PgPoolOptions::new()
.max_connections(50)
.min_connections(10)
.acquire_timeout(Duration::from_secs(5))
.idle_timeout(Duration::from_secs(600))
.max_lifetime(Duration::from_secs(1800))
.connect("postgres://user:password@localhost:5432/mydb")
.await
.expect("Failed to create pool");
Configuration from JSON
Hyperlane supports loading configuration from JSON files using config_from_json. You can use this to externalize database connection settings:
use hyperlane::*;
#[tokio::main]
async fn main() {
let config = config_from_json("config.json").await;
let server = Server::default();
// Apply configuration from JSON
// Database connection strings can be stored in config
// and used to initialize pools
server.run().await;
}
Server Configuration for Database Workloads
When running database-heavy workloads, tune the server configuration appropriately:
use hyperlane::*;
#[tokio::main]
async fn main() {
let mut server = Server::default();
// Configure request settings for database operations
server.request_config = RequestConfig::from_json(r#"{
"buffer_size": 8192,
"max_body_size": 1048576,
"read_timeout_ms": 30000
}"#).await;
// Set server network options
server.server_config.set_address("0.0.0.0:8080");
server.server_config.set_nodelay(true);
server.server_config.set_ttl(64);
server.run().await;
}
Middleware-Based Database Access
Request Middleware for Database Operations
Hyperlane's #[request_middleware(N)] attribute allows you to intercept requests before they reach route handlers. This is useful for database transaction management:
#[request_middleware(1)]
async fn db_transaction_middleware(ctx: Context) -> Result<(), RequestError> {
// Begin a database transaction
// The transaction can be stored in the context
// and committed or rolled back in the response middleware
Ok(())
}
Response Middleware for Cleanup
Use #[response_middleware] for post-request database cleanup:
#[response_middleware]
async fn db_cleanup_middleware(ctx: Context) -> Result<(), RequestError> {
// Commit or rollback transactions
// Release any database resources
Ok(())
}
Error Handling Middleware for Database Errors
Hyperlane's #[request_error] attribute lets you handle database errors gracefully:
#[request_error]
async fn db_error_handler(ctx: Context) -> Result<(), RequestError> {
if let Some(error_data) = ctx.try_get_request_error_data() {
let error_msg = format!("Request error: {}", error_data);
// Log the error using hyperlane-log
log::error!("{}", error_msg);
let mut response = ctx.get_mut_response();
response.set_status_code(500);
response.set_body(r#"{"error": "Internal server error"}"#);
}
Ok(())
}
Task Panic Handling for Database Crashes
Use #[task_panic] to handle panics in database-related tasks:
#[task_panic]
async fn db_panic_handler(ctx: Context) -> Result<(), RequestError> {
if let Some(panic_data) = ctx.try_get_task_panic_data() {
log::error!("Database task panicked: {:?}", panic_data);
// Attempt to reconnect or notify monitoring
}
Ok(())
}
Error Handling for Database Operations
Structured Error Responses
When database operations fail, it's important to return meaningful error responses:
#[route("/users")]
async fn list_users(ctx: Context) -> Result<(), RequestError> {
let result = sqlx::query_as::<_, User>("SELECT * FROM users")
.fetch_all(/* pool */)
.await;
match result {
Ok(users) => {
let mut response = ctx.get_mut_response();
response.set_status_code(200);
response.set_header("content-type", "application/json");
response.set_body(serde_json::to_string(&users).unwrap());
}
Err(e) => {
return Err(RequestError::new(
&format!("Database query failed: {}", e),
500
));
}
}
Ok(())
}
Using RequestError for Database Failures
Hyperlane's RequestError type is designed for exactly this purpose:
#[route("/data")]
async fn get_data(ctx: Context) -> Result<(), RequestError> {
// Attempt database operation
match perform_query().await {
Ok(data) => {
let mut response = ctx.get_mut_response();
response.set_status_code(200);
response.set_body(data);
}
Err(db_err) => {
// Return a structured error
return Err(RequestError::new(
&format!("Query failed: {}", db_err),
500
));
}
}
Ok(())
}
Performance Considerations
Benchmark Context
Hyperlane delivers exceptional performance for database-driven applications. Here are the benchmark numbers that set the baseline:
- Keep-Alive disabled: hyperlane handles 51,031 QPS
- Keep-Alive enabled: hyperlane reaches 334,888 QPS
- ab 1 million requests: hyperlane sustains 316,211 QPS
These numbers mean hyperlane's HTTP layer is rarely the bottleneck — the database layer is typically where optimization matters most.
Linux Kernel Tuning for Database Servers
When deploying database-backed hyperlane applications on Linux, tune the kernel parameters:
# TCP connection tuning
net.ipv4.tcp_max_tw_buckets = 20000
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 262144
# File descriptor limit
ulimit -n 1024000
Build Optimization
Compile with native CPU targeting for maximum performance:
RUSTFLAGS="-C target-cpu=native -C link-arg=-fuse-ld=lld" cargo run --release
Connection Pool Best Practices
-
Size pools appropriately: Set
max_connectionsbased on your database's capacity and expected concurrent load -
Use
min_connections: Pre-warm the pool to avoid cold-start latency -
Set
acquire_timeout: Prevent requests from hanging indefinitely waiting for a connection -
Configure
idle_timeout: Release unused connections to free database resources -
Set
max_lifetime: Recycle connections periodically to prevent issues from accumulated connection state
Caching Strategy
Use Redis caching to reduce database load:
- Cache frequently accessed data with appropriate TTLs
- Use cache-aside pattern: check cache first, then database
- Invalidate cache entries when data is updated
- Set the
X-Cacheheader to indicate cache hits/misses for debugging
Conclusion
Integrating databases with hyperlane is straightforward thanks to Rust's async ecosystem and hyperlane's flexible middleware system. Whether you're using MySQL for relational data, PostgreSQL for advanced queries, or Redis for caching and sessions, the pattern is consistent:
- Initialize connection pools during server startup
- Use middleware for cross-cutting concerns like transactions and sessions
- Handle errors gracefully with
RequestError - Tune connection pool parameters for your workload
- Use Redis caching to reduce database load
Combined with hyperlane's impressive performance — up to 334,888 QPS with Keep-Alive — your database-driven applications will be both fast and reliable. The key to performance lies not in the HTTP layer but in proper database configuration, connection pooling, and caching strategies.
For more information, explore the hyperlane ecosystem tools like hyperlane-log for structured logging, hyperlane-utils for common utilities, and utoipa for API documentation generation.
Project Code:https://github.com/hyperlane-dev/hyperlane
Top comments (0)