MacBobby Chibuzor Go, Solidity, and Haskell developer interested in the cloud native world and blockchain technology. A fanatic for technical writing and open source contribution.

Create an API in Rust with SQLite and Rocket

6 min read 1867 107

Create API Rust SQLite Rocket

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:

Prerequisites

To follow along with this tutorial, you’ll need the following:

  • Familiarity with the Rust programming language and Cargo build system and package manager
  • Basic understanding of database connections
  • Ability to start a development project in your preferred environment

What is SQLite?

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.

Initializing the project with Cargo

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.

Setting up for SQLite

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:

Terminal Output Confirming SQLite Installation

If you’re not familiar with how to use the SQLite3 database, you can learn more about it here.

Connecting to the SQLite database with Rust

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:

Creating SQLite Table Usernames Passwords

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.

Getting familiar with the Rocket framework

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"

Getting familiar with the Diesel framework

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

Building a to-do API

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

Creating migrations with Diesel

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 migrations
  • down.sql: Statements for reverting migrations

You 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.

Writing the API logic in Rust

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:

  1. 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)
}

Routing the Rusty Rocket

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:

Rocket Running Background After API Build

Conclusion

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.

LogRocket: Full visibility into web frontends for Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket. LogRocket Dashboard Free Trial Banner

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust app. 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.

MacBobby Chibuzor Go, Solidity, and Haskell developer interested in the cloud native world and blockchain technology. A fanatic for technical writing and open source contribution.

7 Replies to “Create an API in Rust with SQLite and Rocket”

  1. Whoa. Can’t believe I’ve been doing this the wrong way. Thank you for this guide – it’s timely indeed.

  2. 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.

    1. 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.

  3. Is there a reason for using diesel initially then switching to using sqlx in the application?

  4. 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)

    1. 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)

      1. 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

Leave a Reply