Whether you’re using SQLite because it is the most popular database engine in the world, or you’re working with Rust because it is the most loved language, you can never go wrong with these two technologies. SQLite and Rust will provide you with speed and efficiency.
This tutorial will demonstrate how to use SQLite as the database context system for Rust APIs. We’ll create a SQLite database, set up and install the Rocket framework for writing the server logic, and then use the Diesel framework to handle connections to the SQLite database.
Let’s get started!
Jump ahead:
To follow along with this tutorial, you’ll need the following:
SQLite is a relational database management system with a lightweight feel in terms of setup complexity and resource usage. It is serverless and requires zero configurations. Because SQLite is literally a database residing in a single cross-platform file, it requires no administration.
SQLite transactions are ACID-compliant (atomic, consistent, isolated, and durable), providing safe access to multi-threading operations. As lightweight and “pruned” as it sounds, it has most of the important query language features in the SQL2 standards.
Cargo makes it easier to start a Rust project of any kind. For our SQLite API project, we’ll use the following command for initialization:
cargo new rust-rocket-sqlite-api --bin
This creates the Cargo.toml
file.
SQLite is the default database store in this tutorial, so as a next step, install the SQLite drivers for your machine.
SQLite is pre-installed on new Mac computers, but (if needed) the Mac command is as follows:
brew install sqlite
Here’s the installation command for Linux users:
sudo apt install sqlite3
Now, confirm installation, like so:
sqlite3 --version
Next, run an instance:
sqlite3
You should see the below output:
If you’re not familiar with how to use the SQLite3 database, you can learn more about it here.
Now, let’s connect to the SQLite database.
To start, run an instance of SQLite in the terminal with the following command:
sqlite3
Next, we need a file for the data. Create a folder called data
using the below command and navigate into it:
sqlite filename.db
We’ll create a file named data.db
. Next, we’ll create a table containing usernames and passwords:
create table users(username text PRIMARY KEY, password text);
If we run the .tables
command, we’ll see that users
is now a table in the database engine:
Next, let’s populate the user table with values:
insert into users(username, password) values ("Jon Doe", "j0hnd03"); sqlite> select * from users;
In the Cargo.toml
file, add the SQLite
crate in the dependencies section:
[dependencies] sqlite = "0.30.1"
Now, build the project to be certain there are no errors:
cargo build
This command will download and build the dependency crates and compile the project.
Rocket is a popular framework for Rust backend web development like the Actix Web framework. It’s newer than Actix Web and offers some advantages. For one, it’s easier to write an application in Rocket, since most of the boilerplate code is included under the hood.
Another advantage is that Rocket’s middleware components and handlers are easier to implement, as they do not require any deep understanding of the process.
To install Rocket, simply specify the framework and its version under the dependencies in the Cargo.toml
file. Since we’re using Rocket as the web framework, we do not need to specify SQLite and its version in the dependencies file:
[dependencies] rocket = "0.5.0-rc.2"
Diesel is an ORM framework that can be used to create and handle connections to the SQLite database. Diesel has dependency on libpq
, libmysqlclient
, and libsqlite3
. To install Diesel, we only need to specify it in the dependencies file. We can add Diesel to our Rusty Rocket, like so:
[dependencies] rocket = "0.5.0-rc.2" diesel = { version = "2.0.2", features = ["sqlite"] } dotenvy = "0.15"
Here, we also add the dotenvy
library to manage environment variables. This library looks for a file that ends with .env
to find all the environment variables to load. We’ll create a file named .env
now, and input the following variable:
DATABASE_URL=data.db
We’ll use diesel cli
locally to manage the project. To install it, simply run the following:
cargo install diesel_cli
While this is the general command, we can strip diesel_cli
of any unneeded libraries and specify the one we want. In this case, we need SQLite so we’ll run the following:
cargo install diesel_cli --no-default-features --features sqlite
This command prevents installing the defaults, except for SQLite. Next, we’ll use Diesel to set migrations:
diesel setup
To demonstrate the usage of the packages we’ve installed, we’ll build a to-do API.
Let’s start by creating a new project:
cargo new rusty-rocket --bin
Next, we’ll use Diesel to create a directory to handle migrations. Diesel does this automatically after you run this command:
diesel migration generate create_tasks
The resulting directory is named create_tasks
, and it contains two files:
up.sql
: Statements for applying migrationsdown.sql
: Statements for reverting migrationsYou can write migrations in these files. For example, our CREATE
statement will be in the up.sql
file:
-- Your SQL goes here CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, description TEXT NOT NULL );
And the corresponding statement will be in the down.sql
file:
DROP TABLE tasks;
After you add these statements to the respective files, apply the migrations with the following command:
diesel migration run
To confirm that this works, check the parent directory for a new file named db.sqlite3
. You can add this to your .gitignore
file if you use one, along with the target/
and .env
files.
Also, try running diesel migration redo
to confirm that the down.sql
file correctly undoes the up.sql
file.
So far, we’ve created the database using Diesel. Now, we need to write a module to connect to the database, and also write the to-do API logic.
But first, let’s make the following changes to our Cargo.toml
file:
[package] name = "rusty-rocket" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] rocket = { version = "0.5.0-rc.2", features = ["json"] } sqlx = { version = "0.6", features = ["runtime-tokio-native-tls", "sqlite"] } tokio = { version = "1", features = ["full"] }
All our previous dependencies and more are contained in these parent packages.
Next, we’ll add a module by creating a folder with the module’s name along with a Rust source file with the same name. We’ll create a database.rs
file and a directory named database
. Inside of this directory, we’ll add two files, one for requests and one for responses:
requests.rs
file for requests
:use rocket::serde::Deserialize; #[derive(Deserialize, Debug)] #[serde(crate = "rocket::serde")] pub struct TaskRequest { pub name: String, pub description: String, }
2) responses.rs
file for responses
:
use rocket::serde::Serialize; use sqlx::FromRow; #[derive(Serialize, FromRow, Debug)] #[serde(crate = "rocket::serde")] pub struct Task { pub id: i64, pub name: String, pub description: String, }
Now. we’ll make the module public by calling it in the database.rs
file. We’ll also make connections to the database, as well as write the create
and get
functions for making and polling tasks.
The logic looks like this:
use sqlx::{Pool, Sqlite}; pub mod requests; pub mod responses; use responses::Task; pub type DBResult<T, E = rocket::response::Debug<sqlx::Error>> = std::result::Result<T, E>; pub async fn create_task( pool: &Pool<Sqlite>, name: &String, description: &String, ) -> DBResult<i64> { let mut connection = pool .acquire() .await?; let id = sqlx::query_as!( Task, r#" INSERT INTO tasks (name, description) VALUES (?, ?); "#, name, description ) .execute(&mut connection) .await? .last_insert_rowid(); Ok(id) } pub async fn get_task(pool: &Pool<Sqlite> id: i64) -> DBResult<Task> { let mut connection = pool.acquire() .await?; let task = sqlx::query_as!( Task, r#" SELECT id, name, description from tasks WHERE id = ?; "#, id ) .fetch_one(&mut connection) .await?; Ok(task) } pub async fn get_tasks(pool: &Pool<Sqlite>) -> DBResult<Vec<Task>> { let mut connection = pool.acquire() .await .unwrap(); let tasks = sqlx::query_as::<_, Task>( r#" select id, name, description from tasks; "# ) .fetch_all(&mut connection) .await?; Ok(tasks) }
It’s time to write the routing logic in the main.rs
file with the help of the Rocket framework:
#[macro_use] extern crate rocket; mod database; use database::requests::TaskRequest; use database::responses::Task; use database::{create_task, get_task, get_tasks, DBResult}; use rocket::serde::json::Json; use rocket::State; use sqlx::{Pool, Sqlite, SqlitePool}; #[post("/tasks", format = "json", data = "<task>")] async fn create(task: Json<TaskRequest>, pool: &State<Pool<Sqlite>>) -> DBResult<Json<Task>> { let id = create_task(pool, &task.name, &task.description).await?; let task = get_task(pool, id).await?; Ok(Json(task)) } #[get("/tasks")] async fn index(pool: &State<Pool<Sqlite>>) -> DBResult<Json<Vec<Task>>> { let tasks = get_tasks(pool).await?; Ok(Json(tasks)) } #[get("/tasks/<id>")] async fn detail(id: i64, pool: &State<Pool<Sqlite>>) -> DBResult<Json<Task>> { let task = get_task(pool, id).await?; Ok(Json(task)) } #[rocket::main] async fn main() -> Result<(), rocket::Error> { let pool = SqlitePool::connect("sqlite://db.sqlite3") .await .expect("Couldn't connect to sqlite database"); sqlx::migrate!() .run(&pool) .await .expect("Couldn't migrate the database tables"); let _rocket = rocket::build() .mount("/", routes![index, create, detail]) .manage(pool) .launch() .await?; Ok(()) }
Now, we can proceed to run the project, using the following command in the parent directory:
cargo run
Here’s the output showing Rocket running in the console after the build:
In this tutorial, we demonstrated how to build a simple to-do API using Rocket and Diesel. We saw how Rust handles connections to the SQLite database using Rocket.
This sample project can be structured in any way you like. For example, using the concepts described here, you could create modules for the schema and model instead of writing them in the database.rs
file.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSimplify component interaction and dynamic theming in Vue 3 with defineExpose and for better control and flexibility.
Explore how to integrate TypeScript into a Node.js and Express application, leveraging ts-node, nodemon, and TypeScript path aliases.
es-toolkit is a lightweight, efficient JavaScript utility library, ideal as a modern Lodash alternative for smaller bundles.
The use cases for the ResizeObserver API may not be immediately obvious, so let’s take a look at a few practical examples.
9 Replies to "Create an API in Rust with SQLite and Rocket"
Whoa. Can’t believe I’ve been doing this the wrong way. Thank you for this guide – it’s timely indeed.
Please stop recommending Rocket. It should be considered depreciated. It is no longer actively maintained. I would recommend using something like Axum since it is very well maintained.
Hi Elliot,
We already have an article about using Actix-web framework here: https://blog.logrocket.com/building-rest-api-rust-rhai-actix-web/. However, based on high requests from developers, this one had to be addressed.
Refer to this report on Stackshare to confirm how Rocket is more used before now: https://stackshare.io/stackups/actix-vs-rocket#:~:text=Rocket%20and%20Actix%20can%20be,stars%20and%20216%20GitHub%20forks.
But your point and recommendations are very valid, we only hope you understand ours too.
Is there a reason for using diesel initially then switching to using sqlx in the application?
Hey! I am a beginner with the rust programming language and I find this tutorial great. I followed all the steps, but after I run the “cargo run” command, I get the following error:
error with configuration: unrecognized database url: “test_data.db”
–> src/database.rs:20:14
|
20 | let id = sqlx::query_as!(
| ______________^
21 | | Profile,
22 | | r#”
23 | | INSERT INTO profiles (name, humidity_min, humidity_max, light) VALUES (?, ?, ?, ?);
… |
28 | | light
29 | | )
| |_____^
|
= note: this error originates in the macro `$crate::sqlx_macros::expand_query` which comes from the expansion of the macro `sqlx::query_as` (in Nightly builds, run with -Z macro-backtrace for more info)
sorry my personal changed were applied, it is rather this error:
error: error with configuration: unrecognized database url: “data.db”
–> src/database.rs:18:14
|
18 | let id = sqlx::query_as!(
| ______________^
19 | | Task,
20 | | r#”
21 | | INSERT INTO tasks (name, description) VALUES (?, ?);
… |
24 | | description
25 | | )
| |_____^
|
= note: this error originates in the macro `$crate::sqlx_macros::expand_query` which comes from the expansion of the macro `sqlx::query_as` (in Nightly builds, run with -Z macro-backtrace for more info)
change your .env file to
“`
DATABASE_URL=sqlite://data.db
“`
and the start of your main function to
“`
let mut config = sqlx::sqlite::SqliteConnectOptions::new();
config = config.filename(“data.db”);
let pool = SqlitePool::connect_with(config)
.await
.expect(“Couldn’t connect to sqlite database”);
“`
and ofc you can modify the config as you want
Hi — Thank you for that ‘obvious’ tip, that I didn’t see. I used the `env` file to set my DB to use a relative path as: “sqlite://../data/data.db”. It just works. Gotta love Rust (sometimes).
Diesel or sqlx? You changed the diesel to sqlx or am i missing something?