Skip to content

Build a Rust API with Rocket, Diesel, and MySQL

Learn how to build a Rust API using Rocket, Diesel, and a MySQL database hosted on PlanetScale.

Build a Rust API with Rocket, Diesel, and MySQL

Rust has made itself heir apparent to the C and C++ dynasty by delivering memory safety without compromising speed. Additionally, Rust does away with a garbage collector, which gives it an additional performance boost. These benefits have led to Rust finding more relevance in backend development.

In this article, you'll learn how to build a Rust bird-watching API powered by a PlanetScale MySQL database. After setting up the database, you'll scaffold a Rust project and connect it to your database using Diesel as an ORM (Object Relational Mapper). Finally, using the Rocket framework, you will expose endpoints that can handle requests and, update the database accordingly.

This guide assumes a basic knowledge of Rust, so if you're new to the language, it may be helpful to review the Rust book before proceeding. You will also need the latest version of Rust installed and a PlanetScale account.

Setting up a PlanetScale database

With PlanetScale, you can either create a new database via the dashboard or the CLI (pscale). The dashboard will be used in this article. Go to the PlanetScale dashboard and click "New database" > "Create new database". A modal form will be displayed asking for the database name and region.

Give your database a name and select the region closest to you or your application. For this tutorial, we will name the database bird_db and use the default region. Click “Create database” to complete the creation process. You will be redirected to the database dashboard as shown below.

The dashboard gives you an overview of your database and access to all the features highlighted in the introduction. For now, you need the database credentials. To get them, click the “Connect” button at the top right corner of the dashboard. This shows you the database authentication credentials and sample code for connecting to your database for different programming languages.

Copy these details and keep them handy as we will need them when we start building our application. For security, this page is only displayed once and if you lose your credentials, you will need to regenerate a new set.

Now we have everything we need to build and connect our application to our database.

Setting up the Rust application

Create a new project using the following command:

Terminal
cargo new bird_watcher_api --bin
cd bird_watcher_api

Next, add the project dependencies. Open cargo.toml and add the following dependencies:

TOML
chrono = { version = "0.4.24", features = ["serde"] }
diesel = { version = "2.0.0", features = ["mysql", "chrono"] }
dotenvy = "0.15"
rocket = { version = "0.5.0-rc.2", features = ["json"] }
  1. Chrono will be used for datetime management. Serde is also added as a feature to help with serializing JSON responses and deserializing JSON requests.
  2. Diesel is the ORM that will be used to interact with the database. The mysql feature is specified to provide the requisite API for interacting with a MySQL-based database (such as PlanetScale). The chrono feature enables support for (de)serializing date/time values from the database using types provided by chrono.
  3. Dotenvy helps with loading environment variables from a .env file.
  4. Rocket is the web framework you will use for handling incoming requests and returning responses. The json feature adds JSON support to your application.

Update your dependencies using the following command:

TOML
cargo update

Next, add Diesel to the project. Diesel provides a CLI to help manage database-related aspects of your application. It is recommended that you install it on your system, which you can do with the following command:

Terminal
cargo install diesel_cli --no-default-features --features mysql

This makes the diesel executable available throughout your system.

With Diesel added to the project, you can add your database connection parameters. To do this, create a new file named .env and update it as follows:

Terminal
DATABASE_URL=<<YOUR_PLANETSCALE_DATABASE_URL>>

Next, set Diesel up with the following command.

Terminal
diesel setup

This command connects to the specified URL, creates the database (if it didn’t previously exist), and adds a new table named __diesel_schema_migrations, which is used to keep track of migrations. You can confirm this change from your database dashboard.

Next, create the migration to create a table for birds as well as the seed the table with some birds. Create the migration with the following command.

Terminal
diesel migration generate create_birds

Diesel CLI will create two SQL files for us, which can be found in the migrations folder. You’ll see output that looks something like this:

Terminal
Creating migrations/2023-03-15-164154_create_birds/up.sql
Creating migrations/2023-03-15-164154_create_birds/down.sql

When migrations are applied, the commands in up.sql are run. To undo (revert) the changes made by the commands in up.sql, the commands in down.sql can be called.

Add the following to up.sql.

SQL
CREATE TABLE bird (
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
scientific_name VARCHAR(255) NOT NULL,
commonwealth_status VARCHAR(255) NOT NULL
);
INSERT INTO bird (name, scientific_name, commonwealth_status) VALUES
("Black-eared Miner", "Manorina melanotis", "Critically Endangered"),
("Eastern Bristlebird", "Dasyornis brachypterus", "Endangered"),
("Swift Parrot", "Lathamus discolor", "Endangered"),
("Australasian Bittern", "Botaurus poiciloptilus", "Endangered"),
("Southern Giant Petrel", "Macronectes giganteus", "Endangered"),
("Star Finch (eastern), Star Finch (southern)", "Neochmia ruficauda ruficauda", "Endangered"),
("Gould's Petrel", "Pterodroma leucoptera leucoptera", "Endangered"),
("Mallee Emu-wren", "Stipiturus mallee", "Endangered"),
("Coxen's Fig-Parrot", "Cyclopsitta diophthalma coxeni", "Critically Endangered"),
("Black-throated Finch (southern)", "Poephila cincta cincta", "Endangered"),
("Chatham Albatross", "Thalassarche eremita", "Endangered"),
("Grey Grasswren (Bulloo)", "Amytornis barbatus barbatus", "Endangered"),
("Australian Painted Snipe", "Rostratula australis", "Endangered"),
("Amsterdam Albatross", "Diomedea exulans amsterdamensis", "Endangered"),
("Northern Royal Albatross", "Diomedea epomophora sanfordi", "Endangered"),
("Tristan Albatross", "Diomedea exulans exulans", "Endangered");

Here you specify the structure of the bird table. This table has three columns for the bird name, scientific name, and commonwealth status. Next, you seed the table with some sample birds.

In down.sql, add a command to drop the bird table as shown below.

Terminal
DROP TABLE bird;

Next, run the command for Diesel to execute the migrations in up.sql.

Terminal
diesel migration run

When the migration has run, you can check that the birds have been added to your database from your PlanetScale dashboard.

To complete the integration between your database and the Rust application, update your application to load the list of birds in the database from the terminal. Start by writing a function to establish a connection with your database. In your src folder, please create a new file named database.rs and add the following code.

Rust
use std::env;
use diesel::prelude::*;
use dotenvy::dotenv;
pub fn establish_connection() -> MysqlConnection {
dotenv().ok();
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
MysqlConnection::establish(&database_url)
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}

Using the environment variable declared earlier, a new connection is established and returned for use elsewhere in the application.

Next, create a struct to model a Bird in the application. In the src folder, create a new file named models.rs and add the following code.

Rust
use diesel::prelude::Queryable;
use rocket::serde::Serialize;
#[derive(Serialize, Queryable)]
#[serde(crate = "rocket::serde")]
pub struct Bird {
pub id: i32,
pub name: String,
pub scientific_name: String,
pub commonwealth_status: String,
}

Using the Queryable traits assumes that the order of fields on the struct matches the columns in the associated table, so make sure to define them in the order seen in the src/schema.rs file. The src/schema file is autogenerated by Diesel.

The Serialize trait allows Rocket to serialize the Bird struct into a JSON object.

Next, update src/main.rs to match the following.

Rust
#[macro_use]
extern crate rocket;
use diesel::prelude::*;
use rocket::{Build, Rocket};
use rocket::serde::json::Json;
use self::models::*;
use self::schema::bird::dsl::*;
mod database;
mod models;
mod schema;
#[get("/")]
fn index() -> Json<Vec<Bird>> {
let connection = &mut database::establish_connection();
bird.load::<Bird>(connection).map(Json).expect("Error loading birds")
}
#[launch]
fn rocket() -> Rocket<Build> {
rocket::build().mount("/", routes![index])
}

The main function establishes a connection with the database, queries for the birds in the database, and returns the results in a JSON response.

Run the application again to see the results using the following command.

Rust
cargo run

By default, the application will be run on port 8000. Open http://127.0.0.1:8000 in your browser to see the results.

Add a feature for bird sightings

Now that you have a seeded database with birds, you can add a new feature that allows you to add, view, and remove bird sightings.

Start by adding a new migration using Diesel.

Terminal
diesel migration generate create_bird_sightings

In the newly created up.sql, add the following command.

SQL
CREATE TABLE bird_sighting (
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
bird_id INT NOT NULL,
sighting_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
sighting_location VARCHAR(255),
additional_information TEXT
);

The accompanying down.sql file should contain the DROP command shown below.

SQL
DROP TABLE bird_sighting;

Next, run the command for Diesel to execute the migrations in up.sql.

SQL
diesel migration run

Next, add two structs — one to model a bird sighting, and another to model the input required for creating a new sighting. Update src/models.rs to match the following.

Rust
use diesel::prelude::*;
use rocket::serde::{Deserialize, Serialize};
use chrono;
use crate::schema::bird_sighting;
#[derive(Serialize, Queryable)]
#[serde(crate = "rocket::serde")]
pub struct Bird {
pub id: i32,
pub name: String,
pub scientific_name: String,
pub commonwealth_status: String,
}
#[derive(Serialize, Queryable)]
#[serde(crate = "rocket::serde")]
pub struct BirdSighting {
pub id: i32,
pub bird_id: i32,
pub sighting_date: Option<chrono::NaiveDateTime>,
pub sighting_location: Option<String>,
pub additional_information: Option<String>,
}
#[derive(Insertable, Deserialize)]
#[serde(crate = "rocket::serde")]
#[diesel(table_name = bird_sighting)]
pub struct BirdSightingInput {
pub bird_id: i32,
pub sighting_location: Option<String>,
pub additional_information: Option<String>,
pub sighting_date: Option<chrono::NaiveDateTime>,
}

Next, you will need to add functions that will handle incoming requests for the various actions your API will support. The index function in src/main.rs is an example. However, adding more of these in main.rs can make the code difficult to read, so create a new file to hold these functions. In the src folder, create a new file called controller.rs and add the following code to it.

Rust
use diesel::prelude::*;
use rocket::response::status::NoContent;
use rocket::serde::json::Json;
use crate::database;
use crate::models::*;
#[get("/")]
pub fn index() -> Json<Vec<Bird>> {
use crate::schema::bird::dsl::bird;
let connection = &mut database::establish_connection();
bird.load::<Bird>(connection).map(Json).expect("Error loading birds")
}
#[post("/sighting", data = "<sighting>")]
pub fn new_sighting(sighting: Json<BirdSightingInput>) -> Json<BirdSighting> {
use crate::schema::bird_sighting;
let connection = &mut database::establish_connection();
diesel::insert_into(bird_sighting::table)
.values(sighting.into_inner())
.execute(connection)
.expect("Error adding sighting");
Json(bird_sighting::table
.order(bird_sighting::id.desc())
.first(connection).unwrap()
)
}
#[get("/sighting?<bird>")]
pub fn all_sightings(bird: Option<i32>) -> Json<Vec<BirdSighting>> {
let connection = &mut database::establish_connection();
use crate::schema::bird_sighting::dsl::{bird_id, bird_sighting};
let query_result: QueryResult<Vec<BirdSighting>> = match bird {
Some(id) => {
bird_sighting.filter(bird_id.eq(id)).load(connection)
}
None => bird_sighting.load(connection)
};
query_result.map(Json).expect("Error loading sightings")
}
#[delete("/sighting/<sighting_id>")]
pub fn delete_sighting(sighting_id: i32) -> NoContent {
use crate::schema::bird_sighting::dsl::*;
let connection = &mut database::establish_connection();
diesel::delete(bird_sighting.filter(id.eq(sighting_id)))
.execute(connection).expect("Error deleting sighting");
NoContent
}

Notice that the index function has been moved from main.rs into the newly created file. Additionally, three functions have been added.

The new_sighting function uses Rocket’s guard to deserialize the incoming request as Json<BirdSightingInput> with which a new bird sighting is saved to the database and returned as a response.

The all_sightings function is used to either get all bird sightings, or the sightings for a particular bird (whose ID is specified as a request query parameter bird).

The delete_sighting function deletes a sighting with the provided ID.

Finally, update src/main.rs to match the following.

Rust
#[macro_use]
extern crate rocket;
use rocket::{Build, Rocket};
mod database;
mod models;
mod schema;
mod controller;
#[launch]
fn rocket() -> Rocket<Build> {
rocket::build().mount("/", routes![
controller::index,
controller::new_sighting,
controller::all_sightings,
controller::delete_sighting
])
}

Having moved the index function to src/controller.rs, the main.rs file now contains module attachments and the function responsible for launching Rocket.

You can run your application to test the new features.

Terminal
cargo run

Conclusion

In this article, we have discussed how to build a Rust API using the Rocket framework, Diesel as an ORM, and a MySQL database hosted on PlanetScale. We started by creating a new database on PlanetScale, then set up a Rust project and connected it to the database using Diesel. Finally, we used Rocket to expose endpoints that could handle requests and update the database accordingly.

By following the steps outlined in this article, you should now have a working Rust API that interacts with a MySQL database hosted on PlanetScale. Happy Coding!

Want a powerful and performant database that doesn’t slow you down?