Welcome to the first part of our comprehensive series dedicated to constructing a fully functional API Gateway using the Rust programming language. If you’ve been hunting for a hands-on, step-by-step guide that demystifies the intricacies of building scalable and secure gateways in Rust, you’ve landed in the right place! 🦀
Over the span of this series, we will explore the foundational aspects of an API Gateway and delve into advanced features, ensuring that by the end, you will have a production-ready piece of infrastructure in your toolkit.
In today’s article, we’ll kick off our journey by introducing our gateway’s fundamental components and features, setting the stage for in-depth discussions and code explorations in the upcoming articles.
So, whether you’re a seasoned Rustacean or just venturing into the Rust ecosystem, strap in for an enlightening journey!
What is an API Gateway?
An API Gateway is a server that acts as an intermediary for requests from clients seeking resources from other services. Think of it as a kind of “middleman” or a “gatekeeper” that manages and directs incoming traffic, ensuring that requests are handled efficiently and securely.
Key Functions of an API Gateway:
- Request Routing: Directs incoming requests to the appropriate service based on URL, method, headers, or other rules.
- Load Balancing: Distributes incoming requests to multiple instances of a service, ensuring no single instance gets overwhelmed.
- Authentication & Authorization: Verifies the identity of the requester (authentication) and determines whether they have the right permissions to access a particular resource (authorization).
- Rate Limiting: Restricts a client’s requests in a specified time window, protecting services from potential abuse or overloads.
- Request & Response Transformation: Modifies incoming requests or outgoing responses to adhere to the expected format or to add/remove specific information.
- Caching: Stores frequently-used responses to minimize redundant processing and accelerate response times.
- Circuit Breaking: Detects service failures and prevents the system from overloading those failing services by temporarily pausing requests.
- Logging & Monitoring: Keeps track of all incoming and outgoing requests, helping in monitoring, alerting, and debugging.
- Security: Provides features like SSL termination, CORS management, and protection against attacks such as SQL injection or DDoS attacks.
Why is it Important?
With the rise of microservices architecture, where an application is broken into small, loosely coupled services, managing and coordinating requests becomes more complex. Here’s where the API Gateway shines:
- Simplification: Clients (like front-end apps) no longer need to make requests to multiple services individually. They communicate with the API Gateway, which handles the intricate details.
- Centralized Management: Aspects like authentication, logging, or rate limiting are managed centrally, preventing redundant configurations and streamlining the architecture.
- Flexibility: As services evolve, change, or scale, the API Gateway ensures clients remain unaffected. Service locations can change without the client ever knowing.
- Performance Enhancements: Features like caching result in faster response times, enhancing user experience.
- Robustness: By handling failures gracefully and preventing overloads with mechanisms like rate limiting and circuit breaking, the API Gateway ensures better system uptime.
Crates for the API Gateway Project
hyper
A fast and flexible HTTP library in Rust. It will be our primary tool for setting up the HTTP server, handling requests, and making client connections.
A Rust asynchronous runtime that provides the necessary tools to write reliable and fast asynchronous applications. This crate powers the asynchronous functionality, enabling non-blocking operations crucial for scalable services.
hyper-tls
Provides support for HTTPS to the hyper crate. Allows our gateway to make secure HTTPS client requests and potentially serve HTTPS requests.
A framework for serializing and deserializing Rust data structures efficiently and generically. serde_json provides JSON support. Used for parsing and creating JSON payloads, especially when transforming requests or responses.
jsonwebtoken
A library to use JSON Web Tokens (JWT) in Rust. Handles JWT-based authentication by validating provided JWT tokens.
arc-swap
Provides a way to safely and efficiently swap the content of an Arc (Atomic Reference Counted) pointer. Assists in managing shared state, like our rate limiter, across multiple threads without locking.
1. Setting Up
Firstly, add the necessary dependencies to your Cargo.toml:
[dependencies]
hyper = "0.14"
tokio = { version = "1", features = ["full"] }
serde = "1.0"
serde_json = "1.0"
2. Setting Up the Hyper Server
hyper is a fast, low-level HTTP library. Let's set it up:
use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};
async fn handle_request(req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
// This is where our gateway logic will reside
Ok(Response::new(Body::from("Hello, World!")))
}
#[tokio::main]
async fn main() {
let make_svc = make_service_fn(|_conn| {
// Clone the handle for each connection.
let service = service_fn(handle_request);
async { Ok::<_, hyper::Error>(service) }
});
let addr = ([127, 0, 0, 1], 8080).into();
let server = Server::bind(&addr).serve(make_svc);
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
}
3. Routing
Next, we want to route the incoming requests to different services. We can create a simple match on the request’s path:
async fn handle_request(req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
match req.uri().path() {
"/service1" => {
// Redirect to service 1
},
"/service2" => {
// Redirect to service 2
},
_ => Ok(Response::new(Body::from("Not Found"))),
}
}
4. Service Forwarding
To forward requests to downstream services, use the hyper client:
let client = hyper::Client::new();
let forwarded_req = Request::builder()
.method(req.method())
.uri("http://downstream_service_url")
.body(req.into_body())
.unwrap();
let resp = client.request(forwarded_req).await?;
Expanding our Basic API Gateway
Let’s expand our basic API Gateway by adding a couple more features:
- Basic logging.
- Simple in-memory rate limiting.
Dependencies
Add the following dependencies to your Cargo.toml:
[dependencies]
hyper = "0.14"
tokio = { version = "1", features = ["full"] }
std::collections::HashMap
std::sync::Mutex
Enhance the API Gateway code:
use hyper::{Body, Request, Response, Server, StatusCode};
use hyper::service::{make_service_fn, service_fn};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::net::SocketAddr;
struct RateLimiter {
visitors: Arc<Mutex<HashMap<SocketAddr, u32>>>,
}
impl RateLimiter {
fn new() -> Self {
RateLimiter {
visitors: Arc::new(Mutex::new(HashMap::new())),
}
}
fn allow(&self, addr: SocketAddr) -> bool {
let mut visitors = self.visitors.lock().unwrap();
let counter = visitors.entry(addr).or_insert(0);
if *counter >= 5 { // Allow up to 5 requests
false
} else {
*counter += 1;
true
}
}
}
async fn service_handler(path: &str) -> Result<Response<Body>, hyper::Error> {
match path {
"/service1" => Ok(Response::new(Body::from("Hello from Service 1"))),
"/service2" => Ok(Response::new(Body::from("Hello from Service 2"))),
_ => {
let mut not_found = Response::default();
*not_found.status_mut() = StatusCode::NOT_FOUND;
Ok(not_found)
},
}
}
async fn handle_request(req: Request<Body>, rate_limiter: Arc<RateLimiter>) -> Result<Response<Body>, hyper::Error> {
let remote_addr = req.remote_addr().expect("Remote address should be available");
if !rate_limiter.allow(remote_addr) {
return Ok(Response::builder()
.status(StatusCode::TOO_MANY_REQUESTS)
.body(Body::from("Too many requests"))
.unwrap());
}
println!("Received request from {}:{}", remote_addr.ip(), remote_addr.port());
let response = service_handler(req.uri().path()).await;
response
}
#[tokio::main]
async fn main() {
let rate_limiter = Arc::new(RateLimiter::new());
let make_svc = make_service_fn(move |_conn| {
let rate_limiter = Arc::clone(&rate_limiter);
// Clone the handle for each connection.
let service = service_fn(move |req| handle_request(req, Arc::clone(&rate_limiter)));
async { Ok::<_, hyper::Error>(service) }
});
let addr = ([127, 0, 0, 1], 8080).into();
let server = Server::bind(&addr).serve(make_svc);
println!("API Gateway running on http://{}", addr);
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
}
In this:
- We added a basic logging feature that prints incoming requests’ IP addresses.
- We introduced a simple rate limiter. If an IP address sends more than five requests, it will receive a “Too many requests” response until the rate limiter is reset (in this basic example, when the program restarts).



