4

How to Build a Simple API in Rust (Part 2)

 3 years ago
source link: https://hackernoon.com/how-to-build-a-simple-api-in-rust-part-2-vv9q35ze
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

How to Build a Simple API in Rust (Part 2)

4
heart.pngheart.pngheart.pngheart.png
light.pnglight.pnglight.pnglight.png
boat.pngboat.pngboat.pngboat.png
money.pngmoney.pngmoney.pngmoney.png

@ncribtNARUHODO

Software engineer during the week and apprentice blogger during the weekend. I like JS and Rust. 🤓

Welcome to the second part of the guide on how to build an API in Rust.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

To follow this guide, you will need to have the code from the first part. If you haven’t checked it out yet, please do!

0 reactions
heart.png
light.png
money.png
thumbs-down.png

This time I will explain how to connect the API to MongoDB. I will create two endpoints, one to add data to the database and another one to retrieve the data from the database.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

I will put all the code in the same file (

src/main.rs
). Of course, in a real situation, you should split the code into multiple files/directories and separate the logic between controllers and services.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Prerequisites

To be able to follow the guide, you will need to have Rust and Cargo installed. You will also need to have MongoDB running. If you’re not sure how to install MongoDB, there are 3 ways I can think of:

0 reactions
heart.png
light.png
money.png
thumbs-down.png

1. You can install MongoDB on your machine. Follow the official documentation to do so.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

2. If you have Docker, you can run a MongoDB container. Check out the mongo image documentation. Th.is is the command I use to run the mongo container:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
docker run -d -p 27017:27017 --name mongo mongo

3. If you prefer to avoid installing anything (MongoDB/Docker), you can create a free cluster on MongoDB Atlas.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Set up the environment

We need to create a 

.env
file to store the MongoDB connection string safely. If you're using Git, make sure to add 
.env
in your 
.gitignore
. You should never commit a 
.env
file.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Open the project from part 1 and create the 

.env
file.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
touch .env

Inside the .env file, add the following:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
MONGODB_URI=mongodb://localhost:27017

Make sure to replace the connection string with the correct one depending on how you’re running MongoDB.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

It is good practice to have a 

.env.example
file (that one should be committed) so that when someone pulls the project for the first time, they have an example of how to set up the 
.env
file.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Create a .env.example file

0 reactions
heart.png
light.png
money.png
thumbs-down.png
touch .env.example

Add the following:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
MONGODB_URI= ** MongoDB URI (e.g. mongodb://localhost:27017)

Add the new dependencies

We need to add 2 new dependencies:

dotenv
and
mongodb
.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
dotenv
is a crate that allows us to use a .env file. It adds the variables from the file to the environment variables.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
mongodb
is the official MongoDB Rust driver. It allows us to interact with the database.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Update the

Cargo.toml
file with the two dependencies. The updated dependencies should be like this:
0 reactions
heart.png
light.png
money.png
thumbs-down.png
[dependencies]
tide = "0.16"
async-std = { version = "1", features = ["attributes"] }
serde = { version = "1", features = ["derive"] }
dotenv = "0.15"
mongodb = { version = "1", features = ["async-std-runtime"], default-features = false }

Because we use the

async-std
runtime, we need to disable the default features of the mongodb crate. By default, it uses
tokio
. Then we need to add the
async-std-runtime
feature to use the
async-std
runtime.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Let’s code

We need to make some modifications to the code. First, update the imports with the following:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
use async_std::stream::StreamExt;
use dotenv::dotenv;
use mongodb::bson::doc;
use serde::{Deserialize, Serialize};
use std::env;
use tide::{Body, Request, Response, StatusCode};

These are all the module items we will use in this walk-through.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Remember last time we set up an empty Tide

State
? This time we will use the
State
to pass the database connection to the controllers. We need to update the
State
struct like this:
0 reactions
heart.png
light.png
money.png
thumbs-down.png
#[derive(Clone)]
struct State {
  db: mongodb::Database,
}

Now we need to update the

main
function. We have to create the database connection and add it in the state before creating the Tide app.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Here is the updated

main
function:
0 reactions
heart.png
light.png
money.png
thumbs-down.png
#[async_std::main]
async fn main() -> tide::Result<()> {
  // Use the dotenv crate to read the .env file and add the environment variables
  dotenv().ok();

  // Create the MongoDB client options with the connection string from the environment variables
  let mongodb_client_options =
    mongodb::options::ClientOptions::parse(&env::var("MONGODB_URI").unwrap()).await?;

  // Instantiate the MongoDB client
  let mongodb_client = mongodb::Client::with_options(mongodb_client_options)?;

  // Get a handle to the "rust-api-example" database
  let db = mongodb_client.database("rust-api-example");

  // Create the Tide state with the database connection
  let state = State { db };

  let mut app = tide::with_state(state);

  app.at("/hello").get(hello);

  app.listen("127.0.0.1:8080").await?;

  return Ok(());
}

I’ve added comments in the code to help you understand what’s going on.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

I first called the

dotenv().ok()
method to read from the 
.env
file and add the environment variables. Then I created a connection to the database. Finally, I passed that connection to the Tide
State
so that our controllers have access to it.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

We will now create two controllers, the first one to create documents in the database and the second one to retrieve those documents.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Here is the code of the first controller:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
#[derive(Debug, Serialize, Deserialize)]
// The request's body structure
pub struct Item {
  pub name: String,
  pub price: f32,
}

async fn post_item(mut req: Request<State>) -> tide::Result {
  // Read the request's body and transform it into a struct
  let item = req.body_json::<Item>().await?;

  // Recover the database connection handle from the Tide state
  let db = &req.state().db;

  // Get a handle to the "items" collection
  let items_collection = db.collection_with_type::<Item>("items");

  // Insert a new Item in the "items" collection using values
  // from the request's body
  items_collection
    .insert_one(
      Item {
        name: item.name,
        price: item.price,
      },
      None,
    )
    .await?;

  // Return 200 if everything went fine
  return Ok(Response::new(StatusCode::Ok));
}

First, I’ve defined an

Item
struct that represents the request's body. The body should be a JSON object with a property
name
of type
string
and a property
price
of type
number
.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Inside the function, I first try to read the request’s body. I did not do any validation. In a real case, don’t be like me and always validate the request’s body before using it.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

I recover the database connection from the Tide

State
. Then I get a handle on the
items
collection.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Finally, I attempt to insert a new document in the collection using the request’s body values.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

If everything goes smoothly, I return an HTTP status code 200.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Now let’s create a second controller to retrieve documents from the

items
collection. Here is the code:
0 reactions
heart.png
light.png
money.png
thumbs-down.png
async fn get_items(req: Request<State>) -> tide::Result<tide::Body> {
  // Recover the database connection handle from the Tide state
  let db = &req.state().db;

  // Get a handle to the "items" collection
  let items_collection = db.collection_with_type::<Item>("items");

  // Find all the documents from the "items" collection
  let mut cursor = items_collection.find(None, None).await?;

  // Create a new empty Vector of Item
  let mut data = Vec::<Item>::new();

  // Loop through the results of the find query
  while let Some(result) = cursor.next().await {
    // If the result is ok, add the Item in the Vector
    if let Ok(item) = result {
      data.push(item);
    }
  }

  // Send the response with the list of items
  return Body::from_json(&data);
}

Inside the function, I retrieve the handle to the database connection from the Tide

State
, then I get a handle to the
items
collection.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

I use the

find
function to retrieve all the documents from the
items
collection. After that, I create a new empty Vector of item.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

I loop through the

find
query results, and I insert each
item
in the Vector.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Finally, I send the response with the list of items in the body.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The controllers are ready, but we still need to add them to the

main
function. Add the following after the
hello
route in the
main
function:
0 reactions
heart.png
light.png
money.png
thumbs-down.png
app.at("/items").get(get_items);
app.at("/items").post(post_item);

Voilà! Let’s test it out. Start the server with the following command:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
cargo run

You should now be able to send a

POST
request to
/items
(http://localhost:8080/items). Here is an example of the body (make sure to set the
Content-Type
header to
application/json
)
0 reactions
heart.png
light.png
money.png
thumbs-down.png
{
  "name": "coffee",
  "price": 2.5
}

You should get an empty response with status code 200.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Then try to retrieve the document you just created by doing a

GET
request on
/items
(http://localhost:8080/items).
0 reactions
heart.png
light.png
money.png
thumbs-down.png

You should receive an array with a single entry being the item we just created

0 reactions
heart.png
light.png
money.png
thumbs-down.png
[
  {
    "name": "coffee",
    "price": 2.5
  }
]

Conclusion

That’s it for part two of this guide. I hope it was helpful. If you noticed any errors or something that could be improved, please let me know!

0 reactions
heart.png
light.png
money.png
thumbs-down.png

You can find the full example on GitHub: https://github.com/ncribt/rust-api-example-part-2.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

In the next and last part of the guide, I will show you how to protect the POST /items endpoint using a JWT (JSON Web Token) and a middleware.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Previously published on https://naruhodo.dev/build-an-api-in-rust-part-2/.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
4
heart.pngheart.pngheart.pngheart.png
light.pnglight.pnglight.pnglight.png
boat.pngboat.pngboat.pngboat.png
money.pngmoney.pngmoney.pngmoney.png
by NARUHODO @ncribt. Software engineer during the week and apprentice blogger during the weekend. I like JS and Rust. 🤓Read my stories
Join Hacker Noon

Create your free account to unlock your custom reading experience.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK