Getting started with REST API Web Services in Rust using Axum, PostgreSQL, Redis, and JWT
GitHub: https://github.com/sheroz/axum-rest-api-sample
Rust is gaining increasing popularity each year. Today, Rust is a powerful and proven technology that offers elegant solutions for developing efficient and secure software without the overhead of a garbage collector. The recently released Rust 2024 edition will, I believe, further accelerate Rust's adoption and reinforce its position among other technologies.
Rust is a great tool in the right context. One area where using Rust can be beneficial is in developing mission-critical web backend components or systems that require high levels of efficiency and safety, where consistent performance and resource consumption, as well as robust security, are essential.
However, getting started with Rust development can take some effort for newcomers (even for experienced developers). Learning and entering the ever-growing and diverse Rust ecosystem can also take extra time.
This project demonstrates how to build a REST API web server in Rust using axum, JSON Web Tokens (JWT), SQLx, PostgreSQL, and Redis.
The REST API web server supports JWT-based authentication and authorization, asynchronous database operations for user and account models, a basic transaction example that transfers money between accounts, and detailed API error handling in a structured format.
I hope this project will help you move forward faster in your exciting Rust journey.
Table of contents
- Architecture
- Cross-Origin Resource Sharing (CORS)
- REST API endpoints
- API error handling in structured format
- Configuration and settings
- Authentication & authorization using JSON Web Tokens (JWT)
- Using PostgreSQL database with SQLx
- Logging
- Graceful shutdown
- End-to-end API tests
- Building and running the Web API service
- Using Docker for building and running services
- GitHub CI configuration
Architecture
The project follows the principles of Clean Architecture, which emphasizes separation of concerns and independence of frameworks, databases, and other services. This design approach ensures that the core business logic is independent, easy to test and maintain, and external services can be replaced if necessary.
Project structure
The project is organized into the following main components:
src
: Contains the main application code.api
: API endpoints and request handling (aka presentation).application
: Business logic and application services.domain
: Core business entities and domain logic.infrastructure
: Database and external service integrations.
tests
: Contains end-to-end tests for the API.
Used technologies
- Axum is a web application framework that focuses on ergonomics and modularity. It is built on top of hyper and tokio runtime, and provides a powerful and flexible way to build web services in Rust.
- JSON Web Tokens (JWT) is used for authentication and authorization. It allows secure transmission of information between parties as a JSON object. The project uses jsonwebtoken crate for managing JWT tokens.
- PostgreSQL is used as the primary database for storing application data. The project uses SQLx for all database related operations, including database migrations.
- Redis is used to store revoked JWT tokens. It provides a fast, in-memory data storage.
Cross-Origin Resource Sharing (CORS)
Cross-Origin Resource Sharing (CORS) is handled to allow or restrict resources on a web server depending on where the HTTP request was initiated. The CORS layer is build by using the Tower middleware of the tokio stack. Please look at tower_http::cors module for more details regarding its usage.
In this project, the Cors Layer
is configured to allow any origin, any method, and any header. You can
customize these settings to fit your requirements.
REST API endpoints
- List of available API endpoints: docs/api-docs.md
- API request samples in the format RFC 2616: tests/endpoints.http
Public Endpoints
- Health:
GET /v1/health
- Version:
GET /v1/version
Authentication
- Login:
POST /v1/auth/login
- Refresh Tokens:
POST /v1/auth/refresh
- Logout:
POST /v1/auth/logout
- Revoke Tokens Issued to the User:
POST /v1/auth/revoke-user
- Revoke All Issued Tokens:
POST /v1/auth/revoke-all
- Cleanup Revoked Tokens:
POST /v1/auth/cleanup
Users
- List Users:
GET /v1/users
- Get User by ID:
GET /v1/users/{user_id}
- Add a New User:
POST /v1/users
- Update User:
PUT /v1/users/{user_id}
- Delete User:
DELETE /v1/users/{user_id}
Accounts
- List Accounts:
GET /v1/accounts
- Get Account by ID:
GET /v1/accounts/{account_id}
- Add a New Account:
POST /v1/accounts
- Update Account:
PUT /v1/accounts/{account_id}
Transactions
- Transfer Money:
POST /v1/transactions/transfer
- Get Transaction by ID:
GET /v1/transactions/{transaction_id}
REST API request samples
- Using REST Client for VS Code. Supports RFC 2616 used in request samples: tests/endpoints.http
- Using curl
Health check
Login
List of users
API error handling in structured format
If the processing of the API request fails, REST API endpoints respond with structured API errors, providing consistent and meaningful error handling. The error handling is implemented using serde and thiserror crates.
API error response format
Each error response follows a structured format to ensure consistency and clarity. The API error response structure includes the following fields:
status
: The HTTP status code of the error (e.g., 404, 422).errors
: An array of error entries, each containing detailed information about the error.
Each error entry can include the following fields:
code
: [optional] A unique error code representing the specific error (e.g.,user_not_found
).kind
: [optional] The type of error (e.g.,resource_not_found
,validation_error
).message
: [required] A brief message describing the error.description
: [optional] A detailed description of the error.detail
: [optional] Additional details about the error (e.g., relevant IDs or parameters).reason
: [optional] The reason for the error.instance
: [optional] The URI of the request that caused the error.trace_id
: [optional] A server-generated identifier for tracking the error in logs.timestamp
: [required] The time when the error occurred.help
: [optional] Guidance for resolving the error or further information.doc_url
: [optional] A URL to the documentation for more information about the error.
API error response samples
The optional detail
field can contain a JSON object specifying the parameters that
caused the error or validation failure. Along with the code
field, it helps generate precise and
meaningful error messages for end users, particularly when service consumers need to support multilingual
interfaces.
Please note that the optional, server-generated trace_id
field can be useful for easily matching and
tracking errors between consumers and the service (logs).
The returned error structure may contain multiple error entries in a single error message, which can be useful for simpler and clearer user guidance in the UI.
For the list of error codes please refer to API documentation.
Configuration and settings
Rust ecosystem has some well-known and rich-featured crates, such as clap and config-rs, that can be used to read and easily parse parameters from the CLI or configuration files. Each has its own strengths and use cases.
This project uses environment variables to configure the service. Configuration parameters from the ENV file are loaded into the environment using the dotenvy crate.
The configuration file has the following settings:
Service configuration
SERVICE_HOST
: The hostname or IP address where the service is running.SERVICE_PORT
: The port number on which the service listens.
Redis configuration
REDIS_HOST
: The hostname or IP address of the Redis server.REDIS_PORT
: The port number on which the Redis server listens.
PostgreSQL configuration
POSTGRES_USER
: The username for connecting to the PostgreSQL database.POSTGRES_PASSWORD
: The password for connecting to the PostgreSQL database.POSTGRES_HOST
: The hostname or IP address of the PostgreSQL server.POSTGRES_PORT
: The port number on which the PostgreSQL server listens.POSTGRES_DB
: The name of the PostgreSQL database.POSTGRES_CONNECTION_POOL
: The number of connections in the PostgreSQL connection pool.
JWT (JSON Web Token) configuration
JWT_SECRET
: The secret key used for signing JWTs.JWT_EXPIRE_ACCESS_TOKEN_SECONDS
: The expiration time for access tokens in seconds.JWT_EXPIRE_REFRESH_TOKEN_SECONDS
: The expiration time for refresh tokens in seconds.JWT_VALIDATION_LEEWAY_SECONDS
: The leeway time in seconds for validating JWTs.JWT_ENABLE_REVOKED_TOKENS
: Enable or disable the use of revoked tokens. Set totrue
to allow revoked tokens,false
to disallow.
Authentication & authorization using JSON Web Tokens (JWT)
JSON Web Tokens (JWTs) are widely used for authentication and authorization due to their efficiency, scalability, and ease of use. Their stateless nature makes them self-contained, allowing authentication services to handle millions of requests without maintaining a session state. Since there’s no need to query a database for every request, JWTs enable highly scalable applications.
However, because JWTs do not rely on server-side storage, there is no built-in way to revoke a token before it expires. Once issued, a token remains valid until its expiration time, even if the user logs out on the client side.
While an access token can be removed from the client after logout, this does not prevent unauthorized use if the token is leaked. To mitigate this risk, in some cases, short-lived access tokens can be used to limit exposure, but this method does not guarantee complete invalidation on the server side. If a token is leaked, it can still be used for authentication until its expiration time.
For the applications handling sensitive data that require strict logout enforcement, maintaining a server-side blacklist can help track and revoke tokens before they expire.
Ultimately, choosing a token invalidation strategy involves balancing performance with security requirements, as stricter control often comes at the cost of efficiency.
The project contains examples of how to use JWT, including a strict invalidation case where a server-side, a Redis-based blacklist is maintained. The server-side token invalidation can easily be disabled if not required.
The security module implements all the authentication and authorization-related operations:
- Login, logout, refresh, and revoking operations
- Role based authorization
- Generating and validating access and refresh tokens
- Setting tokens expiry time (based on configuration)
- Using refresh tokens rotation technique
- Revoking issued tokens by using Redis (based on configuration)
- Revoke all tokens issued until the current time
- Revoke tokens belonging to the user issued until the current time
- Cleanup of revoked tokens
Using Redis in-memory storage
The Redis storage is used to keep a list (aka blacklist) of revoked JWT tokens to implement logout and other access restriction operations. Redis allows fast in-memory access to these tokens, ensuring that any revoked token can be quickly checked and invalidated on the server side. This helps maintain a secure authentication system by preventing the use of tokens explicitly revoked before their expiration time. The service::token module implements all Redis-related operations at the service layer, while the redis::connection module handles Redis connections at the infrastructure layer.
Using PostgreSQL database with SQLx
The PostgreSQL database and SQLx crate is used for database operations. However, the project can be easily adapted to use other databases supported by SQLx, such as MySQL or SQLite.
PostgreSQL is a powerful, open-source object-relational database system with a strong reputation for reliability, feature robustness, and performance. It supports advanced data types and performance optimization features, making it an excellent choice for complex applications.
SQLx is an async, pure Rust SQL crate featuring compile-time checked queries without a DSL. It ensures that your SQL queries are correct at compile time, reducing runtime errors and improving code safety. SQLx provides a seamless integration with Rust's async ecosystem.
The project contains examples of the following database operations:
- Database migrations: Using SQLx to manage and ensure the database schema is up-to-date.
- Async CRUD operations: Performing Create, Read, Update, and Delete operations asynchronously.
- Transactions: Handling complex operations that require multiple steps to be executed as a single unit of work.
Logging
The powerful tracing framework is used to collect logs. Tokio tracing allows using different types of subscribers, where logs are written, console, file or some collector on the network. Subscribers can also consist of several layers.
The RUST_LOG
environment variable can be set to specify the desired log level on the launch:
Graceful shutdown
Axum can handle graceful shutdown by providing a future to the with_graceful_shutdown method. The asynchronous signal handler from the tokio::signal module is used to listen for SIGINT (CTRL+C, Unix and Windows) and SIGTERM (Unix) events in the background, signaling the server to initiate the graceful shutdown process.
End-to-end API tests
REST API tests are located at: tests
Using database isolation for tests
Database isolation ensures each test runs by setting up and tearing down the database state before and after each test.
Running tests sequentially
To ensure tests run sequentially and avoid conflicts, the serial_test crate is used by annotating test
functions with #[serial]
.
Running tests
Building and running the Web API service (development)
Building and running the Web API service in debug mode
Running the Web API service in test configuration
Building and running the Web API service in release mode (production)
Building and running a Rust-based web service in release mode is critical to achieving optimal performance in production environments. Release mode in Rust enables various compiler optimizations that significantly improve execution speed and reduce the binary size of the application. While debug mode is useful during development for faster compilation times and debugging capabilities, Rust's release mode ensures that the final web service will work efficiently in production environments. So always build your service in release mode before deploying to ensure that it performs at its best.
Building the Web API service in release mode
Running the Web API service in release mode
Using Docker for building and running services
Please check the docker-compose.yml configuration to build the PostgreSQL and Redis services.
Running the PostgreSQL and Redis services
Building the API service using the official Rust image
The official Rust image can be used to build the API service using the Docker environment. Please check the Dockerfile that used to build the application service.
Please check the docker-compose.full.yml configuration to build the full stack services.
Running the full stack build: API + PostgreSQL + Redis
GitHub CI configuration
The project has continuous integration (CI) using GitHub Actions, which automates the following workflows:
- Running
cargo deny
to check for security vulnerabilities and licenses - Running
cargo fmt
to check for the Rust code format according to style guidelines - Running
cargo clippy
to catch common mistakes and improving the Rust code - Running tests
- Building the application
The GitHub CI file is located at: ci.yml
The source code is available on GitHub: https://github.com/sheroz/axum-rest-api-sample
2023 September (1)
2023 August (1)
2019 May (1)
2016 March (2)
2016 February (1)
2014 December (1)
2013 May (1)
2013 March (1)
2013 February (1)
2012 December (2)
2012 October (1)
2011 February (2)
2010 October (2)
2010 July (1)
2010 May (1)
2010 April (1)