Create a High-Performance REST API with Rust
Building a high-performance REST API is crucial for modern applications that demand speed, scalability, and efficiency. Rust, with its memory safety, zero-cost abstractions, and performance capabilities, is an ideal choice for developers who need to push the boundaries of what their APIs can achieve.
In this tutorial, we'll walk through creating a high-performance REST API using Rust and the Axum web framework, which provides a powerful and ergonomic foundation for building APIs.
What is a REST API?
A REST API (Representational State Transfer Application Programming Interface) is a way for two systems to communicate over HTTP, often used for web services. It allows clients (such as web browsers or mobile apps) to interact with servers by making requests and receiving responses. REST APIs follow a stateless, client-server model and use standard HTTP methods like:
- GET: Retrieve data from the server.
- POST: Send data to the server to create a new resource.
- PUT: Update an existing resource on the server.
- DELETE: Remove a resource from the server.
REST APIs are designed to be simple, scalable, and flexible, making them ideal for building web services that can be consumed by a variety of clients. They use standard formats like JSON or XML for data exchange, allowing for easy integration across different platforms.
In this API that we'll build, we'll use PostgreSQL as the database, SQLX for database interactions, and Axum as the web framework to handle HTTP requests and responses.
What is SQLX?
SQLX is a Rust library that provides a compile-time checked, asynchronous interface for interacting with SQL databases. Unlike traditional ORMs, SQLX directly executes raw SQL queries, offering more control over the database operations while ensuring safety by checking the queries at compile time. This helps catch errors early in the development process, providing the benefits of both SQL and Rust's strict type system.
SQLX supports several databases, including PostgreSQL, MySQL, and SQLite. In this project, we're using it with PostgreSQL, allowing us to perform efficient queries and transactions while benefiting from Rust's performance and safety guarantees.
Overview of the Project
In this tutorial, our goal is to build a high-performance REST API having all the CRUD operations (Create, Read, Update, Delete) for managing posts and users. Here's what we'll build:
- GET /posts: Retrieve a list of all posts.
- GET /posts/:id: Retrieve a specific post by its ID.
- POST /posts: Create a new post.
- PUT /posts: Update an existing post.
- DELETE /posts: Delete an existing post.
- POST /users: Create a new user.
We will be working with two database tables:
- Posts: To store the post content and metadata.
- Users: To manage the users who can create and interact with posts.
Spinning Up a New PostgreSQL Database Using Docker
Before we start building the API, we need a PostgreSQL database to store our data. We'll use Docker to create and run a PostgreSQL container, which will serve as our database server.
Install Docker
If Docker is not installed on your machine, download and install it from Docker's official website. Verify the installation by running:
Start a PostgreSQL Container
Now, let's create and run a PostgreSQL container with the following command:
Here's what each part of the command does:
--name rust-postgres-db
: Gives the container a name.-e POSTGRES_PASSWORD=password
: Sets the password for the PostgreSQLpostgres
user.-e POSTGRES_USER=postgres
: Creates a user namedpostgres
.-e POSTGRES_DB=rust-axum-rest-api
: Creates a database namedrust-axum-rest-api
.-p 5432:5432
: Exposes PostgreSQL on port 5432, which is the default PostgreSQL port.-d postgres
: Runs the container in detached mode using thepostgres
image.
Persisting Data (Optional)
If you want to persist data between container restarts, you can mount a volume. Here's an updated command with volume support:
This command creates a named volume pgdata
that will store your database files, ensuring the data is not lost when the container is stopped.
Setting up the Rust Project
To begin building the API, we need to set up the basic structure for our Rust project. Using Cargo is the most common way to manage Rust projects, as it handles dependencies, building, and running the project.
Create a New Rust Project
To create a new Rust project, run the following command:
This creates a new directory called rust-axum-rest-api
, which includes a basic project structure with:
Setting Up SQLX
To set up SQLX in your Rust project and configure it for PostgreSQL with TLS support, follow these steps:
Add SQLX to the Project
First, add the SQLX dependency with the appropriate features for your setup. Since we're using Tokio as the runtime, PostgreSQL as the database, and native TLS for secure connections, run the following command:
This command adds SQLX to your Cargo.toml
with support for:
- runtime-tokio: Using Tokio as the async runtime.
- tls-native-tls: Using the native TLS library for secure connections.
- postgres: Enabling PostgreSQL as the database.
Install SQLX CLI
To use SQLX features like database migrations or checking SQL queries at compile-time, you can install the SQLX CLI globally:
Once installed, you can run migrations and interact with your PostgreSQL database from the command line.
Create the Database
After setting up your Rust project and configuring SQLX, the next step is to create the PostgreSQL database. SQLX provides a simple command to create the database at the DATABASE_URL
specified in your .env
file.
Ensure the DATABASE_URL is Set
Make sure you have the DATABASE_URL
environment variable set up in your .env
file. The database URL is in the following format:
We'll replace the placeholders with the values of the PostgreSQL database we created earlier:
Run the following command to add the DATABASE_URL
to your .env
file:
Add the .env File to .gitignore
To make sure you don't accidentally commit sensitive information like passwords, add the .env
file to your .gitignore
:
To create the database, simply run the following command:
This will create the rust-axum-rest-api
database as specified in your DATABASE_URL
.
Creating the Tables
Now that the database is set up, it's time to create migrations to define the tables in our schema. We will create two tables: users and posts in separate migration files.
Create the Users Table Migration
First, we will create a migration for the users
table. Run the following command to generate a migration file for the users:
Open the generated migration file and define the schema for the users
table:
Create the Posts Table Migration
Create a migration for the posts
table:
Apply the Migrations
Once the migration files are defined, apply them to the database by running:
This command will apply both migrations, creating the users
and posts
tables in the database.
Migrations Run
Connecting to the database
SQLX is a fully asynchronous library, so we need to install the Tokio runtime first to handle async operations. Add the tokio
dependency to your Cargo.toml
:
Update the main function
Since we use the tokio
runtime, we need to update the main
function to be asynchronous by using the #[tokio::main]
attribute.
Reading the environment variables
In order to read the DATABASE_URL
environment variable, we'll use the dotenvy
crate to load the environment variables from the .env
file. Add the dotenvy
dependency to your Cargo.toml
:
Now, let's load the environment variables from the .env
file and establish a connection to the database.
Creating a Database Connection Pool
Ensuring Database connection
Let's run the code to ensure that the database connection is working as expected.
Connected to the Database
Setting Up the REST API with Axum
Now that we have our database set up and connected, we can now start building the REST API.
Installing Dependencies
To begin, we need to install Axum for handling web requests, Serde for serializing and deserializing JSON, tracing for structured logging, and tracing_subscriber for handling log formatting and output.
Install the necessary dependencies by running:
This will install Axum, serde
(with derive feature for easier struct handling), and tracing
along with tracing_subscriber
for logging.
Writing a Simple "Hello World" with Axum
To test our setup, let's write a simple starter code that responds with "Hello, world!" on the root route and logs when the server is ready.
This looks a bit cluttered, so let's break it down:
Let's see what this block of code is doing:
This code initializes the tracing
subscriber with a maximum log level of INFO
. This will allow us to log messages at the INFO
level and above.
This is pretty easy to understand. We're loading the environment variables from the .env
file, reading the DATABASE_URL
variable, and establishing a connection to the database. If the connection is successful, we log a message saying "Connected to the database!".
This block creates a new Router
and defines a route for the root path /
that responds with the root
handler function.
This code binds the server to port 5000 and starts serving the application. It logs a message saying "Server is running on http://0.0.0.0:5000
when the server is ready.
This code defines the root
handler function that returns the string "Hello, world!" when the root path /
is accessed.
Now that we have a basic setup of Axum, let's move on to building the REST API with CRUD operations for managing posts and users.
Axum's Extension Layer
To share data between handlers which work on different threads, Axum uses Extension Layers to pass shared data between handlers. This allows you to share resources like database connections, configuration, or other data across your application. Ensuring thread safety and efficient resource sharing.
Let's create an extension layer for our database connection pool.
Note that wherever the layer is defined in the router chain, it will make it available to all the handlers defined after it. So, since we want this connection to be available to all the handlers, we define it as the last item of the chain.
Now the database connection can be easily extracted in any handler and shared between threads.
GET /posts Route
Let's create the first route for our API to retrieve a list of all posts.
Defining the Post Struct
Before defining the handle, let's first define the Post
struct which will be the data type that we require from the database and the same data type that we send as the response to the client.
Remember we installed serde
with the feature derive
, that particular feature let's us derive the Serialize
and Deserialize
traits for our struct, so that it can easily be converted to and from JSON.
Handler for GET /posts
Now, let's define a handler function that queries the database for all posts and returns them as a JSON response.
Using the query_as!
macro from sqlx
we can execute a query and map the result to a struct. In this case, we're fetching all the posts from the posts
table and mapping them to the Post
struct. The fetch_all
method fetches all the rows from the query result and returns them as a Vec<Post>
.
Notice the return type of the handler function is Result<Json<Vec<Post>>, StatusCode>
. This allows us to return a JSON response with the list of posts if the query is successful, or an INTERNAL_SERVER_ERROR
status code if an error occurs.
Adding the handler to the Router
Now that our handler is defined, we can add it to the router to handle the GET /posts
route.
Our first route is now ready, but there is no data to be fetched yet. Let's continue with the other routes and in the end we'll test them all.
GET /posts/:id Route
This one is a bit different, because we're accepting some data which is the id
of the post that is coming from the URL path /posts/:id
. Axum provides a way to extract this data from the URL path using the Path
extractor.
The Path
extractor is used to extract a single segment from the URL path. In this case, we're extracting the id
of the post from the URL path /posts/:id
.
POST /posts Route
Now, let's implement the route to create a new post. This route will accept a JSON payload and insert the post into the database.
Since we are accepting some data from the client, we need to deserialize that data which is a JSON payload, in order to do that, we define a new struct for the payload named CreatePost
and derive the Serialize
and Deserialize
traits for it.
Using the Json
extractor, we can extract the JSON payload from the request body and deserialize it into the CreatePost
struct.
This route creates a new post and returns the created data.
PUT /posts/:id Route
This one is a bit similar to the POST /posts
route, but instead of creating a new post, we're updating an existing post. We need to accept the id
of the post to be updated and the updated data in the JSON payload.
DELETE /posts/:id Route
This route will be a bit different, because we are deleting the post, and there is no data to return after the deletion. So, we need to handle the response accordingly.
We want to return a message like successful
or Post deleted successfully
.
We can create a new struct for that response like this:
But this isn't very convenient always creating a new struct for a response. Luckily, there's a better way to create custom JSON responses, by using an external library that provides us with a macro called json!
The library is called serde_json
, add it to your project:
Now we can use it:
Notice the return type of the handler is Result<Json<serde_json::Value>, StatusCode>
. This allows us to return a JSON response with any keys that we want, instead of having to define a struct for each response.
POST /users Route
Let's implement the route to create a new user. This route will accept a JSON payload from the client containing the username
and email
and insert the new user into the database.
Testing the Routes
Now that we've implemented all the necessary routes for creating, updating, fetching, and deleting posts and users, let's try them out and interact with our API.
We'll first add data to the database using the POST
routes and then fetch and manipulate that data using the GET
, PUT
, and DELETE
routes.
To send the requests you can either use a tool like curl
or Postman, or a VSCode extension like Thunder Client. We'll use Thunder Client in this tutorial.
First, make sure the server is running by running the following command in the terminal:
Server running
Adding data
We don't have any data in the database yet, so let's start by adding some users and posts.
- POST /users: Send a
POST
request to/users
with a JSON body containing theusername
andemail
of the new user.
POST /users
Great! Seems like the user was added successfully and our API is working as expected, let's add a new post.
- POST /posts: Let's send a post request to
/posts
with a JSON body containing thetitle
,body
, anduser_id
of the new post.
POST /posts
We'll do this a few times to have more posts in the database.
Fetch Data with GET /posts and GET /posts/:id
After adding some posts, we can fetch all the posts or a specific post using the GET
routes.
Let's test the GET /posts route by sending a GET
request to /posts
. This will return a list of all posts in the database.
GET /posts
- GET /posts/:id: Fetch a specific post by sending a
GET
request to/posts/1
(or any valid post ID). This will return the details of the requested post.
GET /posts/:id
Update Data with PUT /posts/:id
Let's test the update functionality by sending a PUT
request to /posts/:id
with the updated data.
PUT /posts/:id
Delete Data with DELETE /posts/:id
Finally, let's test the delete functionality by sending a DELETE
request to /posts/:id
to delete a specific post.
DELETE /posts/:id
Great! All of our API routes worked exactly as expected, and we were able to interact with the database to create, read, update, and delete posts and users.
Conclusion
In this tutorial, we've built a complete REST API using Rust with Axum and SQLX. We started by setting up the API to handle creating, fetching, updating, and deleting both users and posts. Along the way, we utilized Serde for JSON serialization and Tokio as our async runtime. By using SQLX, we were able to seamlessly interact with our PostgreSQL database, ensuring efficient data management.
By now, you should have a foundational understanding of how to build a high-performance REST API with Rust and Axum.
You can now extend this API and add more functionality, try to challenge yourself to add more routes like updating users, deleting users, or even adding more complex queries to fetch data from the database.
Thanks for reading, if you enjoyed this blog post, you'll enjoy our other blogs at our blogs page, and you can learn Rust by practicing and solving challenges at Rustfinity challenges section.