Rust 🦀 CRUD Rest API with Docker 🐳
Tags: rust, webdev, beginners, devops
Published: 2023-05-04 14:03:52

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:

{% youtube vhNoiBOuW94 %}

<!-- <iframe width="905" height="510" src="https://www.youtube.com/embed/vhNoiBOuW94" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe> -->

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rrfith1zcurulpcguq3k.png)](https://youtu.be/vhNoiBOuW94)

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
```

Step inside the project folder:

```bash
cd rust-crud-api
```

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

```bash
code .
```

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

```toml
postgres = "0.19"
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
```

`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:

```toml
[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"
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1zez2i30ie3t91u318wv.png)](https://youtu.be/vhNoiBOuW94)

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](https://youtu.be/vhNoiBOuW94) (link in the video description).

### ⬇️ Import the dependencies

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

```rust
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;
```

`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:

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

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:

```rust
//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";
```

- `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:

```rust
//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);
            }
        }
    }
}
```

- `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:

```rust
//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())
}
```

- `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):

```rust
//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),
    }
}
```

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:

```rust
//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()),
    }
}
```

- 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:

```rust
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())
}
```

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
```

### 🚫 .dockerignore

Open the .dockerignore file and add the following:

```docker
**/target
```

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):

```docker
# 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"]
```

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:

```yaml
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: {}
```

- 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
```

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
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w36manxnyhppy9pqgn9g.png)](https://youtu.be/vhNoiBOuW94)


### 🏗️ 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
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ptz95nx4macobnrwn8z4.png)](https://youtu.be/vhNoiBOuW94)

### 👟 Run the Rust Container

Now we can run the Rust container:

```bash
docker compose up rustapp
```

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

```bash
docker ps -a
```

Lastly, you can check the postgres database by typing:
```
docker exec -it db psql -U postgres
\dt
select * from users;
```

Here is a screenshot of the output:

[![Rust project structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q3oauxvlee4r1tf484c6.png)](https://youtu.be/vhNoiBOuW94)

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](https://www.postman.com/downloads/).

### 📝 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bqlhz0a6m51z1ceahhh1.png)](https://youtu.be/vhNoiBOuW94)

### 📝 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"
}
```

[![POST request to http://localhost:8080/users](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i4z86grtqrqs4fq9nr73.png)](https://youtu.be/vhNoiBOuW94)

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"
}
```

```
{
    "name": "ccc",
    "email": "ccc@mail"
}
```

### 📝 Get all users

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

[![GET request to http://localhost:8080/users](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ohiwjuxy6w2n2yc5yu6t.png)](https://youtu.be/vhNoiBOuW94)


### 📝 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0awm5r7sua1o2owz9uso.png)](https://youtu.be/vhNoiBOuW94)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gniru8li36i9k36ae0a1.png)](https://youtu.be/vhNoiBOuW94)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ui44mn79rl3m62p0ybby.png)](https://youtu.be/vhNoiBOuW94)

### 📝 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"
}
```

[![PUT request to http://localhost:8080/users/1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9sz2m5o3cotb84nr5e5u.png)](https://youtu.be/vhNoiBOuW94)


### 📝 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8tgpww7s6h3kobams6qn.png)](https://youtu.be/vhNoiBOuW94)


### 🐢 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2dkwpajxfpxjafuybq1i.png)](https://youtu.be/vhNoiBOuW94)

This will open a new window with the database.

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

[![TablePlus](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cnl6zusvqi1jjlgwc0l2.png)](https://youtu.be/vhNoiBOuW94)

Done.

## 🏁 Conclusion

We made it! 

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

If you prefer a video version:

{% youtube vhNoiBOuW94 %}

<!-- <iframe width="905" height="510" src="https://www.youtube.com/embed/vhNoiBOuW94" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe> -->

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](https://youtu.be/vhNoiBOuW94)