Build an API in Rust (Part 1)

In this multi-parts guide I will show you how to build a simple API in Rust. If you're not sure what Rust is, it's a low-level programming language created at Mozilla. It provides performances similar to C++ but is much safer thanks to the way its compiler is designed. It can catch many common runtime errors at compile time.

Disclaimer

I myself am a beginner in Rust and there are still lots of things I need to learn. So it is possible that what I will show you might not be the best way to do things. If you notice something odd, please let me know!

Prerequisites

To be able to follow this guide you should at least know the basics of Rust, if not I would advise checking the Rust Book.

You will also need to have Rust and Cargo installed.

What will this guide cover?

This guide is a bit long so I've decided to split it into 3 parts.

The first part will cover the basics, I will explain how to setup the project and create one endpoint that will return a Hello World! message.

In the second part, I will connect the API to a MongoDB and add two endpoints to create and retrieve documents from the database.

In the third and last part, I will show you how to build an authentication middleware to protect one of the endpoints.

The web framework

I've decided to use tide as the web framework. It is less popular than actix (which is the most popular at the time being) but as I come from JavaScript, I feel more at home with tide as it is a bit similar to express and overall simpler to use in my opinion.

Project setup

Let's start setting up the project.

First, make sure you have Rust and Cargo installed.

Side-note, cargo is the package manager for Rust, it is similar to npm for those who come from JavaScript.

Type the following command to create a new project with cargo

cargo new rust-api-example-part-1

We're now ready to code, it would probably be easier to open this project in your favorite code editor.

If you're using Visual Studio Code, I recommend installing the following extensions:

rust-analyzer provides all the useful features when coding in Rust (code completion, linting, documentation, etc)

Better TOML provides syntax highlighting for .toml files, which is the format used by Cargo for configuration.

Installing the dependencies

Before we start coding, we need to install the dependencies. At this stage we will only need three of them, tide, async-std and serde.

As I explained earlier, tide is the web framework that we will use to build the API.

async-std is an async runtime. Rust does not provide an async runtime by default, so it is up to us to install one. At the moment there are two popular options, tokio and async-std.

This is a bit annoying because it means that you need to make sure all the asynchronous libraries you will use in the project need to be compatible with the async runtime you picked.

tide requires that we use async-std so we will install this runtime.

serde is a framework to serialize and deserialize data (such as converting JSON objects to Rust structs).

Open the Cargo.toml file and add the following under [dependencies]

tide = "0.16"
async-std = { version = "1", features = ["attributes"] }
serde = { version = "1.0", features = ["derive"] }

You can specify extra features like I did for async-std, in this case we will need the attributes feature.

Make sure it runs

By default when creating the project with cargo, you should have a default src/main.rs file containing an example function. Let's make sure it runs with

cargo run

It should print Hello, world! in your terminal.

Now you can delete that function so we can start setting up our API.

Let's code!

In the src/main.rs file, add the following

#[async_std::main]
async fn main() -> tide::Result<()> {
let app = tide::new();
app.listen("127.0.0.1:8080").await?;

return Ok(());
}

Our main function is now asynchronous because we've added the async keyword in front. For it to work correctly we need to add a runtime, which is exactly what #[async_std::main] does. It is an "attribute" macro that simply wraps our main function in an asynchronous runtime.

The function returns tide::Result<()> which means it will return either nothing (Ok(())) or a Tide error.

Inside the function, we create a new Tide instance then we call the function listen to start the local server on the port 8080.

If you try to contact the API now, you will get a 404, because we didn't define any route yet.

Let's create our first controller, add the following above the main function in src/main.rs

use tide::Request;

#[derive(Clone, Debug)]
struct State {}

async fn hello(_req: Request<State>) -> tide::Result {
return Ok("Hello world!".into());
}

The hello controller is asynchronous and takes one parameter. Since we don't actually use the parameter we can add a leading underscore per convention. The _req parameter is of type Request (imported from tide), it expects a State which is why I've defined an empty State struct.

The State struct needs to implement the Clone trait so we use the derive marco from serde to do so.

The controller return type needs to be tide::Result.

As for the implementation, we simply return a &str that we transform into a tide::Result with the .into() function.

That's all for our controller. Now we will need to make a few changes to our main function. Change it with the following

#[async_std::main]
async fn main() -> tide::Result<()> {
let mut app = tide::with_state(State {});

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

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

return Ok(());
}

I've modified the initialization of our app to pass an empty State. This will be useful later when we will need to pass around our database connection.

Then I've added our endpoint GET /hello.

Now if you try to run the app and make a request to http://localhost:8080/hello you should get Hello world! as a response.

In conclusion

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

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