DEV Community

Cover image for Rust 🦀 CRUD Rest API with Docker 🐳
Francesco Ciulla
Francesco Ciulla

Posted on • Updated on

Rust 🦀 CRUD Rest API with Docker 🐳

Let's create a CRUD Rest API in Rust using:

  • No specific framework
  • Serde to serialize and deserialize JSON
  • Postgres (database)
  • Docker
  • Docker Compose

If you prefer a video version:

All the code is available in the GitHub repository (link in the video description): https://youtu.be/vhNoiBOuW94


🏁 Intro

Here is a schema of the architecture of the application we are going to create:

PHP CRUD Rest API with Laravel, Postgres, Docker and Docker Compose. Postman and Tableplus to test it

We will create five endpoints for basic CRUD operations:

  • Create
  • Read all
  • Read one
  • Update
  • Delete

We will use Postgres as our database and Docker and Docker Compose to run the application.

We will use Postman to test the endpoints and Tableplus to check the database.

👣 Steps

We will go with a step-by-step guide so that you can follow along.

Here are the steps:

  1. Check the prerequisites
  2. Project creation and dependency installation
  3. Code the application
  4. Run the Postgres database with Docker
  5. Build and run the application with Docker Compose
  6. Test the application with Postman and TablePlus

💡 Prerequisites

  • Rust compiler installed (version 1.51+)
  • cargo installed (version 1.51+)
  • docker installed (version 20.10+ )
  • [optional] VS Code installed (or any IDE you prefer)
  • [optional] Postman or any API test tool
  • [optional] Tableplus or any database client

🚀 Create a new Rust project

To create a new Rust project, we will use the CLI.

cargo new rust-crud-api
Enter fullscreen mode Exit fullscreen mode

Step inside the project folder:

cd rust-crud-api
Enter fullscreen mode Exit fullscreen mode

And open the project with your favorite IDE. If you use VS Code, you can use the following command:

code .
Enter fullscreen mode Exit fullscreen mode

Open the file called Cargo.toml and add the following dependencies:

postgres = "0.19"
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
Enter fullscreen mode Exit fullscreen mode

postgres is the Postgres driver for Rust.
serde is a library to serialize and deserialize.
serde_json is a library specific for JSON.
serde_derive is a library to derive the Serialize and Deserialize traits (macro)

Your Cargo.toml file should look like this:

[package]
name = "rust-crud-api"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
postgres = "0.19"
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
Enter fullscreen mode Exit fullscreen mode

Please notice that the Package name could differ based on the name you gave to your project.

Your project should now look like this:

Rust project structure

We are now ready to code the application.


👩‍💻 Code the application

We will go step by step:

  1. Import the dependencies.
  2. Create the model (a user with Id, name, and email) and add constants.
  3. Main function: database connection and TCP server.
  4. Utility functions: set_database, get_id, get_user_request_body.
  5. Create the routes in a function (endpoints).
  6. Create utility functions.
  7. Create the controllers.

For this project, we will code everything in a single file of ~200 lines of code.

This is not a best practice, but it will help us focus on the Rust code, not the project structure.

All the code is available on GitHub (link in the video description).

⬇️ Import the dependencies

Open the main.rs file, delete all the code, and add the following imports:

use postgres::{ Client, NoTls };
use postgres::Error as PostgresError;
use std::net::{ TcpListener, TcpStream };
use std::io::{ Read, Write };
use std::env;

#[macro_use]
extern crate serde_derive;
Enter fullscreen mode Exit fullscreen mode

Client is used to connect to the database.
NoTls is used to connect to the database without TLS.
PostgresError is the error type returned by the Postgres driver.
TcpListener and TcpStream to create a TCP server.
Read and Write are used to read and write from a TCP stream.
env is used to read the environment variables.

the #[macro_use] attribute is used to import the serde_derive macro.

We will use it to derive our model's Serialize and Deserialize traits.

🥻 Create the model

Just below the imports, add the following code:

//Model: User struct with id, name, email
#[derive(Serialize, Deserialize)]
struct User {
    id: Option<i32>,
    name: String,
    email: String,
}
Enter fullscreen mode Exit fullscreen mode

We will use this model to represent a user in our application.

  • id is an integer and is optional. The reason is that we don't provide the id when we create or update a new user. The database will generate it for us. But we still want to return the user with an id when we get them.

  • name is a string, and it is mandatory. We will use it to store the name of the user.

email is a string, and it is mandatory. We will use it to store the user's email (there is no check if it's a valid email).

🪨 Constants

Just below the model, add the following constants:

//DATABASE URL
const DB_URL: &str = env!("DATABASE_URL");

//cosntants
const OK_RESPONSE: &str = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n";
const NOT_FOUND: &str = "HTTP/1.1 404 NOT FOUND\r\n\r\n";
const INTERNAL_ERROR: &str = "HTTP/1.1 500 INTERNAL ERROR\r\n\r\n";
Enter fullscreen mode Exit fullscreen mode
  • DB_URL is the URL of the database. We will read it from the environment variables. In this case, we add the header Content-Type: application/json to the response.
  • OK_RESPONSE, NOT_FOUND, and INTERNAL_ERROR are the responses we will send back to the client. We will use them to return the status code and the content type.

🏠 Main function

Just below the constants, add the following code:

//main function
fn main() {
    //Set Database
    if let Err(_) = set_database() {
        println!("Error setting database");
        return;
    }

    //start server and print port
    let listener = TcpListener::bind(format!("0.0.0.0:8080")).unwrap();
    println!("Server listening on port 8080");

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                handle_client(stream);
            }
            Err(e) => {
                println!("Unable to connect: {}", e);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • set_database is a function that we will create later. It will be used to connect to the database.
  • TcpListener::bind is used to create a TCP server on port 8080.
  • listener.incoming() is used to get the incoming connections.

⛑️ Utility functions

Now, out of the main function, add the three following utility functions:

//db setup
fn set_database() -> Result<(), PostgresError> {
    let mut client = Client::connect(DB_URL, NoTls)?;
    client.batch_execute(
        "
        CREATE TABLE IF NOT EXISTS users (
            id SERIAL PRIMARY KEY,
            name VARCHAR NOT NULL,
            email VARCHAR NOT NULL
        )
    "
    )?;
    Ok(())
}

//Get id from request URL
fn get_id(request: &str) -> &str {
    request.split("/").nth(2).unwrap_or_default().split_whitespace().next().unwrap_or_default()
}

//deserialize user from request body without id
fn get_user_request_body(request: &str) -> Result<User, serde_json::Error> {
    serde_json::from_str(request.split("\r\n\r\n").last().unwrap_or_default())
}
Enter fullscreen mode Exit fullscreen mode
  • set_database connects to the database and creates the users table if it doesn't exist.
  • get_id is used to get the id from the request URL.
  • get_user_request_body is used to deserialize the user from the request body (without the id) for the Create and Update endpoints.

🚦 Handle client

Between the main function and the utility functions, add the following code (no worries, there will be the final code at the end of the article):

//handle requests
fn handle_client(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    let mut request = String::new();

    match stream.read(&mut buffer) {
        Ok(size) => {
            request.push_str(String::from_utf8_lossy(&buffer[..size]).as_ref());

            let (status_line, content) = match &*request {
                r if r.starts_with("POST /users") => handle_post_request(r),
                r if r.starts_with("GET /users/") => handle_get_request(r),
                r if r.starts_with("GET /users") => handle_get_all_request(r),
                r if r.starts_with("PUT /users/") => handle_put_request(r),
                r if r.starts_with("DELETE /users/") => handle_delete_request(r),
                _ => (NOT_FOUND.to_string(), "404 not found".to_string()),
            };

            stream.write_all(format!("{}{}", status_line, content).as_bytes()).unwrap();
        }
        Err(e) => eprintln!("Unable to read stream: {}", e),
    }
}
Enter fullscreen mode Exit fullscreen mode

We create a buffer and then a string for the incoming requests.

Using the match statement in Rust, we can check the request and call the right function to handle it.

If we don't have a match, we send back a 404 error.

Last, we set the stream to write the response back to the client and handle any error.

🎛️ Controllers

Now, let's create the functions that will handle the requests.

They are five functions, one for each endpoint:

  • handle_post_request for the Create endpoint
  • handle_get_request for the Read endpoint
  • handle_get_all_request for the Read All endpoint
  • handle_put_request for the Update endpoint
  • handle_delete_request for the Delete endpoint

Add the code below the handle_client function:

//handle post request
fn handle_post_request(request: &str) -> (String, String) {
    match (get_user_request_body(&request), Client::connect(DB_URL, NoTls)) {
        (Ok(user), Ok(mut client)) => {
            client
                .execute(
                    "INSERT INTO users (name, email) VALUES ($1, $2)",
                    &[&user.name, &user.email]
                )
                .unwrap();

            (OK_RESPONSE.to_string(), "User created".to_string())
        }
        _ => (INTERNAL_ERROR.to_string(), "Internal error".to_string()),
    }
}

//handle get request
fn handle_get_request(request: &str) -> (String, String) {
    match (get_id(&request).parse::<i32>(), Client::connect(DB_URL, NoTls)) {
        (Ok(id), Ok(mut client)) =>
            match client.query_one("SELECT * FROM users WHERE id = $1", &[&id]) {
                Ok(row) => {
                    let user = User {
                        id: row.get(0),
                        name: row.get(1),
                        email: row.get(2),
                    };

                    (OK_RESPONSE.to_string(), serde_json::to_string(&user).unwrap())
                }
                _ => (NOT_FOUND.to_string(), "User not found".to_string()),
            }

        _ => (INTERNAL_ERROR.to_string(), "Internal error".to_string()),
    }
}

//handle get all request
fn handle_get_all_request(_request: &str) -> (String, String) {
    match Client::connect(DB_URL, NoTls) {
        Ok(mut client) => {
            let mut users = Vec::new();

            for row in client.query("SELECT id, name, email FROM users", &[]).unwrap() {
                users.push(User {
                    id: row.get(0),
                    name: row.get(1),
                    email: row.get(2),
                });
            }

            (OK_RESPONSE.to_string(), serde_json::to_string(&users).unwrap())
        }
        _ => (INTERNAL_ERROR.to_string(), "Internal error".to_string()),
    }
}

//handle put request
fn handle_put_request(request: &str) -> (String, String) {
    match
        (
            get_id(&request).parse::<i32>(),
            get_user_request_body(&request),
            Client::connect(DB_URL, NoTls),
        )
    {
        (Ok(id), Ok(user), Ok(mut client)) => {
            client
                .execute(
                    "UPDATE users SET name = $1, email = $2 WHERE id = $3",
                    &[&user.name, &user.email, &id]
                )
                .unwrap();

            (OK_RESPONSE.to_string(), "User updated".to_string())
        }
        _ => (INTERNAL_ERROR.to_string(), "Internal error".to_string()),
    }
}

//handle delete request
fn handle_delete_request(request: &str) -> (String, String) {
    match (get_id(&request).parse::<i32>(), Client::connect(DB_URL, NoTls)) {
        (Ok(id), Ok(mut client)) => {
            let rows_affected = client.execute("DELETE FROM users WHERE id = $1", &[&id]).unwrap();

            //if rows affected is 0, user not found
            if rows_affected == 0 {
                return (NOT_FOUND.to_string(), "User not found".to_string());
            }

            (OK_RESPONSE.to_string(), "User deleted".to_string())
        }
        _ => (INTERNAL_ERROR.to_string(), "Internal error".to_string()),
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Some use the get_id function to get the id from the request URL.

  • The get_user_request_body function is used to get the user from the request body in JSON format and deserialize it into a User struct.

  • There is some error handling in case the request is invalid, or the database connection fails.

📝 Recap

Here is the complete main.rs file:

use postgres::{ Client, NoTls };
use postgres::Error as PostgresError;
use std::net::{ TcpListener, TcpStream };
use std::io::{ Read, Write };
use std::env;

#[macro_use]
extern crate serde_derive;

//Model: User struct with id, name, email
#[derive(Serialize, Deserialize)]
struct User {
    id: Option<i32>,
    name: String,
    email: String,
}

//DATABASE URL
const DB_URL: &str = env!("DATABASE_URL");

//constants
const OK_RESPONSE: &str = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n";
const NOT_FOUND: &str = "HTTP/1.1 404 NOT FOUND\r\n\r\n";
const INTERNAL_ERROR: &str = "HTTP/1.1 500 INTERNAL ERROR\r\n\r\n";

//main function
fn main() {
    //Set Database
    if let Err(_) = set_database() {
        println!("Error setting database");
        return;
    }

    //start server and print port
    let listener = TcpListener::bind(format!("0.0.0.0:8080")).unwrap();
    println!("Server listening on port 8080");

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                handle_client(stream);
            }
            Err(e) => {
                println!("Unable to connect: {}", e);
            }
        }
    }
}

//handle requests
fn handle_client(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    let mut request = String::new();

    match stream.read(&mut buffer) {
        Ok(size) => {
            request.push_str(String::from_utf8_lossy(&buffer[..size]).as_ref());

            let (status_line, content) = match &*request {
                r if r.starts_with("POST /users") => handle_post_request(r),
                r if r.starts_with("GET /users/") => handle_get_request(r),
                r if r.starts_with("GET /users") => handle_get_all_request(r),
                r if r.starts_with("PUT /users/") => handle_put_request(r),
                r if r.starts_with("DELETE /users/") => handle_delete_request(r),
                _ => (NOT_FOUND.to_string(), "404 not found".to_string()),
            };

            stream.write_all(format!("{}{}", status_line, content).as_bytes()).unwrap();
        }
        Err(e) => eprintln!("Unable to read stream: {}", e),
    }
}

//handle post request
fn handle_post_request(request: &str) -> (String, String) {
    match (get_user_request_body(&request), Client::connect(DB_URL, NoTls)) {
        (Ok(user), Ok(mut client)) => {
            client
                .execute(
                    "INSERT INTO users (name, email) VALUES ($1, $2)",
                    &[&user.name, &user.email]
                )
                .unwrap();

            (OK_RESPONSE.to_string(), "User created".to_string())
        }
        _ => (INTERNAL_ERROR.to_string(), "Internal error".to_string()),
    }
}

//handle get request
fn handle_get_request(request: &str) -> (String, String) {
    match (get_id(&request).parse::<i32>(), Client::connect(DB_URL, NoTls)) {
        (Ok(id), Ok(mut client)) =>
            match client.query_one("SELECT * FROM users WHERE id = $1", &[&id]) {
                Ok(row) => {
                    let user = User {
                        id: row.get(0),
                        name: row.get(1),
                        email: row.get(2),
                    };

                    (OK_RESPONSE.to_string(), serde_json::to_string(&user).unwrap())
                }
                _ => (NOT_FOUND.to_string(), "User not found".to_string()),
            }

        _ => (INTERNAL_ERROR.to_string(), "Internal error".to_string()),
    }
}

//handle get all request
fn handle_get_all_request(_request: &str) -> (String, String) {
    match Client::connect(DB_URL, NoTls) {
        Ok(mut client) => {
            let mut users = Vec::new();

            for row in client.query("SELECT id, name, email FROM users", &[]).unwrap() {
                users.push(User {
                    id: row.get(0),
                    name: row.get(1),
                    email: row.get(2),
                });
            }

            (OK_RESPONSE.to_string(), serde_json::to_string(&users).unwrap())
        }
        _ => (INTERNAL_ERROR.to_string(), "Internal error".to_string()),
    }
}

//handle put request
fn handle_put_request(request: &str) -> (String, String) {
    match
        (
            get_id(&request).parse::<i32>(),
            get_user_request_body(&request),
            Client::connect(DB_URL, NoTls),
        )
    {
        (Ok(id), Ok(user), Ok(mut client)) => {
            client
                .execute(
                    "UPDATE users SET name = $1, email = $2 WHERE id = $3",
                    &[&user.name, &user.email, &id]
                )
                .unwrap();

            (OK_RESPONSE.to_string(), "User updated".to_string())
        }
        _ => (INTERNAL_ERROR.to_string(), "Internal error".to_string()),
    }
}

//handle delete request
fn handle_delete_request(request: &str) -> (String, String) {
    match (get_id(&request).parse::<i32>(), Client::connect(DB_URL, NoTls)) {
        (Ok(id), Ok(mut client)) => {
            let rows_affected = client.execute("DELETE FROM users WHERE id = $1", &[&id]).unwrap();

            //if rows affected is 0, user not found
            if rows_affected == 0 {
                return (NOT_FOUND.to_string(), "User not found".to_string());
            }

            (OK_RESPONSE.to_string(), "User deleted".to_string())
        }
        _ => (INTERNAL_ERROR.to_string(), "Internal error".to_string()),
    }
}

//db setup
fn set_database() -> Result<(), PostgresError> {
    let mut client = Client::connect(DB_URL, NoTls)?;
    client.batch_execute(
        "
        CREATE TABLE IF NOT EXISTS users (
            id SERIAL PRIMARY KEY,
            name VARCHAR NOT NULL,
            email VARCHAR NOT NULL
        )
    "
    )?;
    Ok(())
}

//Get id from request URL
fn get_id(request: &str) -> &str {
    request.split("/").nth(2).unwrap_or_default().split_whitespace().next().unwrap_or_default()
}

//deserialize user from request body without id
fn get_user_request_body(request: &str) -> Result<User, serde_json::Error> {
    serde_json::from_str(request.split("\r\n\r\n").last().unwrap_or_default())
}
Enter fullscreen mode Exit fullscreen mode

We are done with the app code. Now it's the turn of Docker.

🐳 Docker

We will build the Rust app directly inside the image. We will use an official Rust image as the base image. We will also use the official Postgres image as a base image for the database.

We will create three files:

  • .dockerignore: to ignore files and folders that we don't want to copy in the image filesystem
  • Dockerfile: to build the Rust image
  • docker-compose.yml: to run the Rust and Postgres services (containers)

You can create them using the terminal or your code editor.

touch .dockerignore Dockerfile docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

🚫 .dockerignore

Open the .dockerignore file and add the following:

**/target
Enter fullscreen mode Exit fullscreen mode

This is to avoid copying the target folder in the image filesystem.

🐋 Dockerfile

We will use a multi-stage build. We will have:

  • a build stage: to build the Rust app
  • a production stage: to run the Rust app

Open the Dockerfile and add the following (explanations in comments):

# Build stage
FROM rust:1.69-buster as builder

WORKDIR /app

# Accept the build argument
ARG DATABASE_URL

# Make sure to use the ARG in ENV
ENV DATABASE_URL=$DATABASE_URL

# Copy the source code
COPY . .

# Build the application
RUN cargo build --release


# Production stage
FROM debian:buster-slim

WORKDIR /usr/local/bin

COPY --from=builder /app/target/release/rust-crud-api .

CMD ["./rust-crud-api"]
Enter fullscreen mode Exit fullscreen mode

Please notice that we are using rust-crud-api as the executable's name. This is the name of the project folder. If you have a different name, please change it.

🐙 docker-compose.yml

Populate the docker-compose.yml file with the following:

version: '3.9'

services:
  rustapp:
    container_name: rustapp
    image: francescoxx/rustapp:1.0.0
    build:
      context: .
      dockerfile: Dockerfile
      args:
        DATABASE_URL: postgres://postgres:postgres@db:5432/postgres
    ports:
      - '8080:8080'
    depends_on:
      - db

  db:
    container_name: db
    image: 'postgres:12'
    ports:
      - '5432:5432'
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=postgres
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata: {}
Enter fullscreen mode Exit fullscreen mode
  • We have two services, rustapp and db. The rustapp service is built using the Dockerfile we created before. The db service uses the official Postgres image. We are using the depends_on property to ensure the db service is started before the rustapp service.

  • Notice that the DATABASE_URL build argument is set to postgres://postgres:postgres@db:5432/postgres. db is the name of the service (and the container_name) of the Postgres container so that it will be resolved to the container IP address.

  • We use the arg property to pass the DATABASE_URL build argument to the Dockerfile.

  • We also use a named volume, pg_data, to persist the database data.

Now it's time to build the image and run the containers.

🏗️ Build the image and run the containers

We need just three more steps:

  • run the postgres container
  • build the Rust app image
  • run the Rust app container

🐘 Run the Postgres container

First, run the postgres container:

docker-compose up -d db
Enter fullscreen mode Exit fullscreen mode

This will pull (download) the image from DockerHub and run it on our machine.

To see the logs, you can type

docker-compose logs db
Enter fullscreen mode Exit fullscreen mode

If you have something like this, it means that the database is up and running in the container (the last line of the logs should say: "database system is ready to accept connections")

Rust project structure

🏗️ Build the Rust app image

It's time to build the Rust app image. We will use the docker-compose build command.
This will build the image using the Dockerfile we created before.

(Note: we might type docker compose up, but by doing that, we would skip understanding what's happening. In a nutshell, when we type docker compose up, Docker builds the images if needed and then runs the containers).

docker compose build
Enter fullscreen mode Exit fullscreen mode

This takes time because we are building the Rust app inside the image.

After ~150 seconds (!), we should have the image built.

Rust project structure

👟 Run the Rust Container

Now we can run the Rust container:

docker compose up rustapp
Enter fullscreen mode Exit fullscreen mode

You can check both containers by opening another terminal and typing:

docker ps -a
Enter fullscreen mode Exit fullscreen mode

Lastly, you can check the postgres database by typing:

docker exec -it db psql -U postgres
\dt
select * from users;
Enter fullscreen mode Exit fullscreen mode

Here is a screenshot of the output:

Rust project structure

It's now time to test our application.

🧪 Test the application

To test the application, we will use Postman. You can download it from here.

📝 Test the db connection

Since we don't have a dedicated endpoint to test the db connection, we will make a GET request to http://localhost:8080/users

The output should be []. This is correct, as the database is empty.

GET request to http://localhost:8080/users

📝 Create a new user

To create a new user, make a POST request to http://localhost:8080/users with the following body:

⚠️ Add the header "Content-Type: application/json" in the request

{
    "name": "aaa",
    "email": "aaa@mail"
}
Enter fullscreen mode Exit fullscreen mode

POST request to http://localhost:8080/users

Create two more users with the following bodies at the same endpoint making a POST request to http://localhost:8080/users

{
    "name": "bbb",
    "email": "bbb@mail"
}
Enter fullscreen mode Exit fullscreen mode
{
    "name": "ccc",
    "email": "ccc@mail"
}
Enter fullscreen mode Exit fullscreen mode

📝 Get all users

To get all the users, make a GET request to http://localhost:8080/users

GET request to http://localhost:8080/users

📝 Get a single user (with error handling)

To get a single user, we can specify the id in the URL.

For example, to get the user with id 1, we can make a GET request to http://localhost:8080/users/1

GET request to http://localhost:8080/users/1

Notice that if we try to get a user with an id that doesn't exist, we get an error.

Make a GET request to http://localhost:8080/users/10

GET request to http://localhost:8080/users/10

And if we try to get a user py using a string instead of an integer, we also get an error.

Make a GET request to http://localhost:8080/users/aaa

GET request to http://localhost:8080/users/aaa

📝 Update a user

We must pass an id in the URL and a body with the new data to update an existing user.

For example, make a PUT request to http://localhost:8080/users/2 with the following body:

{
    "name": "NEW",
    "email": "NEW@mail"
}
Enter fullscreen mode Exit fullscreen mode

PUT request to http://localhost:8080/users/1

📝 Delete a user

Finally, to delete a user, we need to pass the id in the URL.

For example, make a DELETE request to http://localhost:8080/users/3

DELETE request to http://localhost:8080/users/3

🐢 Test with TablePlus

You can also test the application with TablePlus.

Create a new Postgres connection with the following credentials:

  • Host: localhost
  • Port: 5432
  • User: postgres
  • Password: postgres
  • Database: postgres

And click the connect button at the bottom right.

TablePlus

This will open a new window with the database.

You can check the users table and see that the data is there.

TablePlus

Done.

🏁 Conclusion

We made it!

We created a REST API with Rust, Serde, Postgres and Docker.

If you prefer a video version:

All the code is available in the GitHub repository (link in the video description): https://youtu.be/vhNoiBOuW94

That's all.

If you have any questions, drop a comment below.

Francesco

Top comments (44)

Collapse
 
liyasthomas profile image
Liyas Thomas

We are building an open source {free} Postman alternative: Hoppscotch - API request builder for web.

If you'd like to try an online API development / testing platform - we're building Hoppscotch.io.

GitHub logo hoppscotch / hoppscotch

👽 Open source API development ecosystem - https://hoppscotch.io

Collapse
 
francescoxx profile image
Francesco Ciulla

I know the project

Collapse
 
francescoxx profile image
Francesco Ciulla

where cna I find more about Hoppscotch?

Collapse
 
liyasthomas profile image
Liyas Thomas
Thread Thread
 
francescoxx profile image
Francesco Ciulla

thank you. are you looking for collaborations?

Collapse
 
teethew profile image
Thiago Crispim

Great article, it's really good to see how simple it can be to work with Rust even for some applications that we would consider of higher level, like a REST API. I'd just like to point out that as I think this article may be suitable to beginners, you could've mentioned that you didn't follow best practices for REST APIs (like responses in plain text) for the sake of simplicity. Nonetheless, keep up the content!

Collapse
 
francescoxx profile image
Francesco Ciulla

hey Thiago, thanks for taking time to give feedback. yes the focus here is the connection between the rust app and the db, I didn't see many resources about this so I created one. in the future I might do something more focused on best practices, here I am focused on simplicity

Collapse
 
francescoxx profile image
Francesco Ciulla

are you currently building something with Rust?

Collapse
 
teethew profile image
Thiago Crispim

I am building an open source authorization server. First I'm doing it with Typescript (Node) to understand what are the main problems building these. After that I'm going to rewrite it entirely in Rust.

Thread Thread
 
francescoxx profile image
Francesco Ciulla

I also have an article/video about typescript, if you are interested. IT's the same concept so you might find it useful.

Collapse
 
lucasgazzola profile image
Lucas Gazzola

Hi! I think the post is great! It's very interesting, and I may try to build it in a near future. However, I have a question. Could someone inject sql code with the request? Sorry for asking, I'm new in this world, and I love security

Collapse
 
francescoxx profile image
Francesco Ciulla

reply by ChatGPT4

This code does not appear to be vulnerable to SQL injection as it uses prepared statements, which are a security feature that helps prevent SQL injection attacks.

For example, in the handle_post_request function, the SQL query is prepared using placeholders and then executed with the user's inputs as parameters. The execute method of the Postgres client takes care of properly escaping the user inputs and preventing SQL injection attacks. Similarly, all other SQL queries in the code use prepared statements with placeholders.

Overall, it is good practice to use prepared statements or parameterized queries to prevent SQL injection attacks.

Collapse
 
lucasgazzola profile image
Lucas Gazzola

Thanks a lot. I really appreciate your explanation! I'm new, I started programming with the frontend pack we all know. Although, I find backend, database and talking to the os fascinating, specially Rust.

Thread Thread
 
francescoxx profile image
Francesco Ciulla

you are welcome! did you already coded something in Rust?

Collapse
 
francescoxx profile image
Francesco Ciulla

so we should be safe

Collapse
 
kwhandy profile image
Handy Wardhana

from this article, i found rust very challenging just like approaching a girl

if she single, you're fuckdup
if she had bf, you also fuckdup

Collapse
 
francescoxx profile image
Francesco Ciulla

this comment is gold ahah! I think it's because I used no frameworks but I really like Rust

Collapse
 
ikehakinyemi profile image
17th_streetCode

And I truly appreciate you for not using a framework. Not that I don’t appreciate frameworks, but most articles out there are always based on frameworks. But learning without frameworks (most times) provides comprehensive knowledge.

Thread Thread
 
francescoxx profile image
Francesco Ciulla

you are welcome

Collapse
 
lucaszapico profile image
LucasZapico

I came for threads like this. Learning Rust is a challenge for future me. But I can enjoy the memes and I’ll keep a pulse on the sentiment. I’m honestly extremely intimidated, sticking with GoLang for now.

Thread Thread
 
francescoxx profile image
Francesco Ciulla

For me it's just different but it has some great concepts I like. Why do you find it intimidating?

Thread Thread
 
lucaszapico profile image
LucasZapico

I’m getting most of my Rust info from @theprimeagen and I don’t know if I have the computing fundamentals down enough for it not to be a large undertaking. Lots of review of core concepts and such. Which is fine, just a bandwidth issue. I’ve been working on getting GoLang under my belt which I’m happy with. I’ve bookmarked your posts and when I’m ready to dive in I’ll circle back on your content.

Thread Thread
 
francescoxx profile image
Francesco Ciulla

nice, I know him. great livestreamer

Collapse
 
ghadzhigeorgiev profile image
Georgi Hadzhigeorgiev

Articles like this inspire me to look and dig deeper in Rust, thanks!

Collapse
 
francescoxx profile image
Francesco Ciulla

thank you so much! are you currently building somethign with Rust?

Collapse
 
ghadzhigeorgiev profile image
Georgi Hadzhigeorgiev

Nothing specific yet, just spending some time to learn it and discover language capabilities.

Thread Thread
 
francescoxx profile image
Francesco Ciulla

oh good luck then!

Collapse
 
ibrahmad18_28 profile image
ibrahmad18

check this part : All the code is available on GitHub (link in the video description)., we have added a Youtube video not Gihtub link . Else Thank you very good post .

Collapse
 
francescoxx profile image
Francesco Ciulla

yes, the link is generally available on the youtube description

Collapse
 
vivi9876 profile image
Viv.esProcSPL

Great Article !

Collapse
 
francescoxx profile image
Francesco Ciulla

you are welcome

Collapse
 
madzimai profile image
Netsai

Great article,it's so helpful.

Collapse
 
francescoxx profile image
Francesco Ciulla

you are welome

Collapse
 
robsongrangeiro profile image
Robson Grangeiro

It's the perfect post! Clean, precise, have some details... Very nice work. Thanks to sharing.

Collapse
 
francescoxx profile image
Francesco Ciulla

you are welcome Robson

Collapse
 
snaptikkim profile image
tomas raul

very usefull project

Collapse
 
francescoxx profile image
Francesco Ciulla

you are welcome Tomas. do you know Rust?

Collapse
 
shehab_badawy profile image
shehab-badawy

Great tutorial, i dived in without knowledge of rust and i think i got most of it. It would be wonderful if you could make something like that about implementing authentication. Thank you

Collapse
 
francescoxx profile image
Francesco Ciulla

you are welcome! We'll see!

Collapse
 
qiaopengjun profile image
Qiao Pengjun 乔鹏军

why Error: error connecting to server: failed to lookup address information: nodename nor servname provided, or not known

Collapse
 
francescoxx profile image
Francesco Ciulla

it might be relative to the configuration in the docker compose file. did you try to clone the repo and do a docker compose build, then docker compose up?

Collapse
 
rd2765 profile image
rd2765 • Edited

Great work!, am getting an error on build:
error[E0308]: mismatched types
--> src\main.rs:19:22
|
19 | const DB_URL: &str = std::env::var("DATABASE_URL");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected &str, found Result<String, VarError>
|
= note: expected reference &'static str
found enum Result<std::string::String, VarError>

Any ideas?

Collapse
 
francescoxx profile image
Francesco Ciulla

try this line (with the exclamation mark)

const DB_URL: &str = env!("DATABASE_URL");

Collapse
 
balogunmaliko profile image
Balogun Malik O

Hi, thank you for making this tutorial. however, there are a lot of things i don't understand. I'm following this steps but i get error a lot. For example, I do not get the trick behind settting the postgres db url . Can you kindly help out with that?
do i hve to spin up docker first?