Creating and Running a Server
Project Code:https://github.com/hyperlane-dev/hyperlane
Introduction
The server is the heart of any web application. In Hyperlane, creating and running a server is designed to be straightforward while providing the flexibility needed for complex deployment scenarios. This article covers all the ways to create a server, how to start and stop it, and how to run multiple servers concurrently.
Server Creation Methods
Hyperlane provides three distinct ways to create a server, each suited to different use cases.
Method 1: Default Server
The simplest way to create a server is using Server::default(). This creates a server with sensible default settings:
let mut server: Server = Server::default();
let server_control_hook: ServerControlHook = server.run().await.unwrap_or_default();
server_control_hook.wait().await;
The default server is perfect for development, testing, and simple applications. It uses predefined settings for address, request limits, and connection handling, so you can focus on building your application logic rather than configuration.
Method 2: From ServerConfig
When you need to customize server-level settings like the bind address, nodelay, or TTL, create a server from a ServerConfig:
let server_config: ServerConfig = ServerConfig::default();
let mut server: Server = Server::from(server_config);
This approach gives you full control over the server's behavior. You can configure the address, TCP options, and other settings before the server starts. The Server::from() trait implementation handles the conversion automatically.
Method 3: From RequestConfig
If your primary concern is request-level settings (buffer sizes, body size limits, timeouts), create a server from a RequestConfig:
let request_config: RequestConfig = RequestConfig::default();
let mut server: Server = Server::from(request_config);
This is useful when you want to fine-tune how the server handles individual requests without changing server-level settings.
Starting a Server
Once you've created a server instance, you need to start it. The run() method is the key:
let server_control_hook: ServerControlHook = server.run().await.unwrap_or_default();
The run() method is asynchronous and returns a Result<ServerControlHook, ...>. The ServerControlHook is a control handle that lets you manage the server after it starts.
The ServerControlHook
The ServerControlHook provides two essential operations:
- wait(): Keeps the server running until it's explicitly shut down.
- shutdown(): Gracefully stops the server.
// Start and keep running
let server_control_hook: ServerControlHook = server.run().await.unwrap_or_default();
server_control_hook.wait().await;
Keeping the Server Alive
After calling run(), you need to keep the server process alive. The wait() method on ServerControlHook blocks the current task until the server is shut down:
server_control_hook.wait().await;
This is typically the last statement in your main function. Without it, the program would exit immediately after run() returns.
Graceful Shutdown
Hyperlane supports graceful shutdown through the ServerControlHook. When you call shutdown(), the server stops accepting new connections and finishes processing existing ones before stopping:
let server_control_hook: ServerControlHook = server.run().await.unwrap_or_default();
server_control_hook.shutdown().await;
This is important for production deployments where you need to restart or stop the server without dropping in-flight requests. The shutdown process ensures that:
- No new connections are accepted
- Existing connections are given time to complete
- Resources are properly cleaned up
Attribute Macro Style
Hyperlane's attribute macro system provides a more declarative way to set up your server:
use hyperlane::*;
use hyperlane_macros::*;
#[hyperlane(server: Server)]
#[hyperlane(server_config: ServerConfig)]
#[tokio::main]
async fn main() {
server_config.set_nodelay(Some(false));
server.server_config(server_config);
let server_control_hook: ServerControlHook = server.run().await.unwrap_or_default();
server_control_hook.wait().await;
}
In this style:
-
#[hyperlane(server: Server)]injects aservervariable into the function scope -
#[hyperlane(server_config: ServerConfig)]injects aserver_configvariable - You can configure the server directly before calling
run()
This approach is particularly useful when combined with other attribute macros for routing, middleware, and request handling.
Running Multiple Servers
Hyperlane makes it easy to run multiple server instances concurrently using Tokio's spawn and join! macros. This is useful for serving different applications on different ports:
let app1 = tokio::spawn(async move {
let mut server_config: ServerConfig = ServerConfig::default();
server_config.set_address(Server::format_bind_address(DEFAULT_HOST, 80));
let mut server: Server = Server::default();
server.server_config(server_config);
let server_control_hook: ServerControlHook = server.run().await.unwrap_or_default();
server_control_hook.wait().await;
});
let app2 = tokio::spawn(async move {
let mut server_config: ServerConfig = ServerConfig::default();
server_config.set_address(Server::format_bind_address(DEFAULT_HOST, 81));
let mut server: Server = Server::default();
server.server_config(server_config);
let server_control_hook: ServerControlHook = server.run().await.unwrap_or_default();
server_control_hook.wait().await;
});
let _ = tokio::join!(app1, app2);
In this example:
-
app1runs on port 80 -
app2runs on port 81 - Both servers run concurrently as separate Tokio tasks
-
tokio::join!waits for both tasks to complete
The Server::format_bind_address(DEFAULT_HOST, port) helper method makes it easy to construct bind addresses programmatically.
Use Cases for Multi-Server
Running multiple servers is useful in several scenarios:
- HTTP and HTTPS: Run an HTTP server on port 80 and an HTTPS server on port 443.
- API versioning: Serve different API versions on different ports.
- Microservices: Run multiple small services within the same process.
- Health checks: Run your main application on one port and a health check endpoint on another.
Complete Example
Here's a complete example that demonstrates the full lifecycle of creating, configuring, running, and shutting down a server:
use hyperlane::*;
#[tokio::main]
async fn main() {
// Create server with custom configuration
let mut config: ServerConfig = ServerConfig::default();
config.set_address("0.0.0.0:8080");
config.set_nodelay(Some(true));
config.set_ttl(Some(128));
let mut server: Server = Server::from(config);
// Start the server
let server_control_hook: ServerControlHook = server.run().await.unwrap_or_default();
// Server is now running and accepting connections
// In a real application, you might register routes and middleware here
// Keep the server alive
server_control_hook.wait().await;
}
Error Handling
When starting a server, the run() method returns a Result. The unwrap_or_default() call provides a default ServerControlHook if the server fails to start:
let server_control_hook: ServerControlHook = server.run().await.unwrap_or_default();
In production code, you should handle the error explicitly:
match server.run().await {
Ok(hook) => {
println!("Server started successfully");
hook.wait().await;
}
Err(e) => {
eprintln!("Failed to start server: {:?}", e);
}
}
Common reasons for server startup failures include:
- Port already in use
- Insufficient permissions to bind to the address
- Invalid configuration values
Conclusion
Creating and running a server in Hyperlane is designed to be simple yet flexible. Whether you need a quick default server for development or a carefully configured multi-server setup for production, Hyperlane provides the tools you need. The ServerControlHook gives you clean control over the server's lifecycle, and the attribute macro system offers a declarative alternative for those who prefer it.
In the next article, we'll explore Hyperlane's middleware system, which is essential for implementing cross-cutting concerns like authentication, logging, and CORS.
Project Code:https://github.com/hyperlane-dev/hyperlane
Top comments (0)