Getting started with REST API Web Services in Rust using Axum, PostgreSQL, Redis, and JWT

DRAFT


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. I believe the upcoming 2024 edition will 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.


I hope this starter project will help you move forward faster in your exciting Rust journey.


Table of contents


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 login, logout, refresh, and revoking operations.
  • 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 for caching and managing tokens. It provides fast, in-memory data storage and retrieval.


API error handling in structured format

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, server-generated trace_id field can be useful for easily matching and tracking errors between consumers and the service (logs).


Note that 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.


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.


REST API endpoints


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


Health check


Login


List of users


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 to true to allow revoked tokens, false to disallow.

Authentication & authorization using JSON Web Tokens (JWT)

The project contains examples of the following 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 PostgreSQL database with SQLx

The project contains examples of the following database operations:


  • Database migrations
  • Async connection pooling
  • Async CRUD operations and transactions

Using Redis in-memory storage

The project contains examples of the following Redis operations:

  • Async Redis operations

Logging

The tracing crate is used for logging.


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


Running the service

Running the service in debug mode


Running the service in test configuration


Running the service at a specific log level

Setting the RUST_LOG - logging level on the launch:


Using Docker

The project contains examples of using Docker:


  • PostgreSQL and Redis services
  • Building the application using the official Rust image
  • Running the full stack: API + PostgreSQL + Redis

Running the Docker based full stack build


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



IT / Coding / How-To


Other Topics