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.


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 free 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:
cargo new bird_watcher_api --bin
cd bird_watcher_api
Next, add the project dependencies. Open cargo.toml
and add the following dependencies:
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"] }
- Chrono will be used for datetime management. Serde is also added as a feature to help with serializing JSON responses and deserializing JSON requests.
- 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). Thechrono
feature enables support for (de)serializing date/time values from the database using types provided bychrono
. - Dotenvy helps with loading environment variables from a
.env
file. - 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:
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:
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:
DATABASE_URL=<<YOUR_PLANETSCALE_DATABASE_URL>>
Next, set Diesel up with the following command.
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.
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:
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
.
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.
DROP TABLE bird;
Next, run the command for Diesel to execute the migrations in up.sql
.
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.
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.
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.
#[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.
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.
diesel migration generate create_bird_sightings
In the newly created up.sql
, add the following command.
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.
DROP TABLE bird_sighting;
Next, run the command for Diesel to execute the migrations in up.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.
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.
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.
#[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.
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!