In this post, we'll learn how to use Rust Warp web framework.
We will start with the current official example at GitHub. Then, we are going to learn how to modularize it.
You can contact me with Telegram if you need to hire a full stack blockchain dev.
If you want to test Warp more after you read this post, you can refer to this.
You can read Spanish version of it here.
If you are already familiar with Rust and want to save your time, please, just clone the Rust Full Stack repository and refer to Warp and microservice example there.
$git clone https://github.com/steadylearner/Rust-Full-Stack.git
Then, once inside warp folder use these:
$cd hello_world
$cargo run --release
To build a Full Stack Rust app, refer to How to use Rust Yew for the Rust frontend part. You can use its Fetch API and request data from Rust servers to render its pages.
Prerequisite
- How to install Rust
- Warp documentation and Warp official examples
- The article to help you find Futures, Services and Filters better
- FP in Rust
- Reading Rust function signatures
- Real world Tacit programming part 1 and Real world Tacit programming part 2
- Rust closures are hard
- Rust Future crate, Future by Example and Rust future and_then
First, you must install Rust, if you haven't it yet. The blog post How to install Rust will help you. If necessary, visit Rust Website for more information.
I'll assume that you are already familiar with Rust. If you aren't, please, learn how to use it first.
Before reading this post, I hope you read Warp documentation and Warp official examples entirely. After installing the dependency, use this command:
$cargo doc -p warp --open
It will be helpful to spend more time reading the routing example from the author. Notice that the author uses Closures as a main API of the framework. I've made several documentations available to help you better understand how they work.
To learn more about async programming with Rust, please refer to this.
Hope you read all of them before you complete this post.
Table of Contents
- Start with the official example
- Refactor it
- Make Routes to filter user requests
- Build Handlers to respond to them
- Link them to end the API with macro
- Test it work with CURL and Rust
- Conclusion
The project structure for this post is opinionated.
I found that Warp projects can be organized in a similar way to Express ones. Assuming you are already familiar with Rust module systems, this won't be difficult to do.
(I hope you also spend time to read Express documentation.)
You're about to learn how to modulize your Rust app along with Warp. After that, you will be able to organize your Rust projects easily.
Skip the first and second part if you're already familiar with Rust.
1. Start with the official example
We'll make our first Warp app using the official example given by its author. We'll use several commands and create some files.
First, use $cargo new hello_world to make a minimal Rust boilerplate. The cargo should have made src/main.rs and Cargo.toml file.
Verify that Rust is working well in your machine using $cargo c, if you are new to Rust.
Then, add dependencies to your Rust project by pasting it from the author to Cargo.toml. (If you are familar with JavaScript and NPM, you can imagine that this is similar to package.json).
tokio = { version = "0.2", features = ["macros"] }
warp = "0.2.1"
In addition, you can consider cargo edit commands instead.
Now, copy and paste this to main.rs.
use warp::Filter; // 1.
#[tokio::main] // 2.
async fn main() {
let hello = warp::path!("hello" / String) // 3.
.map(|name| format!("Hello, {}!", name)); // 4.
warp::serve(hello) // 5.
.run(([0, 0, 0, 0], 8000)) // 6.
.await;
}
Everything is ready. Use $cargo run --release. Just read this post instead of waiting for it to finish.
(It took about 17 minutes in my laptop, which is over half a decade old. I hope your machine is faster than that.)
When finished, you can use CURL command below to check the end point /hello/String.
$curl 0.0.0.0:8000/hello/www.steadylearner.com // return "Hello, www.steadylearner.com"
I hope your first Warp app worked.
The code used here is very simple. But, a little help to find out how they work will be very useful.
1. If you've read its documentation, the author says the following.
The fundamental building block of warp is the Filter
they can be combined and composed to express rich requirements on requests.
Therefore, it is used for this minimal example to demonstrate how it works and you'll see that later as well.
2. Warp uses tokio as its async task runner behind the scenes. You won't have to worry about it too much to make the example work except "its alternative is async std and they are not that compatible currently".
To learn more about async programming with Rust, please, read all the documenations at async folder at Rust Full Stack.
3. If you are new to Warp framework, it may be difficult to find out what it does. You can might think "hello" as a static path and String as a part to show your intention to receive dynamic parameters. It must be String type only.
If you are familiar with other web framework such as Express, compare it to req(request) part.
The difference is that you express only what you want to allow. Then, use with the API provided by Warp. Compare it to routes/hello_route.rs later.
4. The Rust closure is used here. (If you are familiar with Express, compare it to res(respond) part.)
See that you could use name variable here inside || to use String parameter we allowed before. Afterwards, you could express what you want to do with this:
format!("Hello, {}!", name)
Compare it to handlers/hello_handler.rs later.
If you are new to Rust, you may wonder how the closure works without typing the variable, params, return value and function signature etc here.
This is because the Rust compiler infers types for them instead of you. If you've read the documentations on this, it will be easy to find out how they work.
With the help of this, the Warp API becomes very easy with closures and will be similar to using arrow functions in JavaScript.
But, unlike JavaScript, it's not easy to make the closures reusable throughout the project. Therefore, we'll use functions and macros instead.
5. Before, we made a hello API and saved it to hello variable and learned how they were made. Let the Warp serve it with this:
6. I prefer to use 0.0.0.0 instead of localhost and its alias 127.0.0.1 to dockerize apps easily.
I hope this brief explanation has helped you.
Please, read all the documentation provided at prerequisite.
2. Refactor it
Previously, we managed to make Warp's official example work and learned the details of each part.
We'll refactor the example using functions instead of the closure used there. At the end of the process, the entire project will look like this:
├── Cargo.toml
├── src
│ ├── api
│ │ ├── hello.rs
│ │ └── mod.rs
│ ├── handlers
│ │ ├── hello_handler.rs
│ │ └── mod.rs
│ ├── main.rs
│ ├── routes
│ │ ├── hello_route.rs
│ │ └── mod.rs
│ └── tests
│ ├── hello_test.rs
│ └── mod.rs
You can see that we will make several folders like routes/, handlers/, api/, tests/ etc and files inside them.
You can see a lot of mod.rs there. But, the contents of this will be very simple with pub mod hello_route etc. Think about them just as helpers to export other files inside each folder.
If you let me explain it with more information below, it will be
Rust wants you to be specific with everything, exporting and importing the modules too. If you find its module system difficult, first research what represents them. It will be mod.rs in each folder and main.rs or lib.rs on the top level directory of your Rust project.
crate will represent main.rs or lib.rs and will work as if they were published crate or similar to package in JavaScript. self will be used to represent main.rs or lib.rs itself inside themselves and use modules imported in them.
You may want to use crate keyword but it won't work because it is already used inside extern crate somecrate syntax.
If you want more information about this, please refer to [How to modularize your Rust Frontend] or Rust Yew frontend example or read official Rust documentation for it.
So, without those mod.rs files, what we need will be just these:
├── src
│ ├── api
│ │ ├── hello.rs
│ ├── handlers
│ │ ├── hello_handler.rs
│ ├── main.rs
│ ├── routes
│ │ ├── hello_route.rs
│ └── tests
│ ├── hello_test.rs
First, we'll start with main.rs. This will help you to find the structure of the entire project better.
use warp::{self, Filter};
use console::Style;
mod routes; // 1.
mod handlers; // 2.
mod api; // 3.
use self::{
routes::{
hello_route,
},
handlers::{
hello_handler
},
};
// It will only work with $cargo test
// For example, $cargo test hello -- --nocapture
#[cfg(test)] mod tests;
#[tokio::main]
async fn main() {
let target: String = "0.0.0.0:8000".parse().unwrap();
let blue = Style::new()
.blue();
// 4.
let end = hello!().with(warp::log("hello"));
println!("\nRust Warp Server ready at {}", blue.apply_to(&target));
println!("Use $curl 0.0.0.0:8000/hello/www.steadylearner.com to test the end point.");
warp::serve(end).run(([0, 0, 0, 0], 8000)).await;
}
You can see that it is very simple because all parts are already modularized.
First, we include folder (submodules) we will make with mod keyword. You can replace it in your mind with this.
Import folder and the Rust files of it along in the same directory. I should have already done **mod.rs** to represent the folder and written **pub mod filename** to export Rust files there, to satisfy the Rust compiler.
1. routes/ will include what you want the server to accept from the user's request. We already know that we will only accept "/hello/String route. You can compare it with the req part of Express.
2. handlers/ will include what you want server to do with it. We already know that we will return the html (text) with format!("Hello, {}!", name) and name part will be String filtered by hello_route we will make.
This will be the payload of your project.
Later, you'll spend most of time with databases and models/ to use them. Refer to Warp database example if you want.
3. The macro hello! that we are going to do will be usable in main.rs and tests/hello_test.rs by importing api folder with that.
This is maybe optional. But, it will help you not to write the same code below in tests/ folder we'll make.
let hello = hello_route::hello()
.and_then(hello_handler::hello)
You may wonder "Why use macro here, when functions, etc can be used?"
This was the last, and perhaps the simplest solution I came up with. It wasn't easy to insert the code because some types used are private, unstable and not allowed by the compiler.
Consult compare folder before you would finding the best way.
4. The variable name end is used here because it will be used before you finally serve it with warp::serve.
You can refer to this from Warp database example.
let post_api = list_posts!()
.or(get_post!())
.or(create_post!())
.or(update_post!())
.or(delete_post!());
let end = post_api.with(warp::log("post_api"));
Other parts are just to help you debug the app better.
3. Routes to filter user requests
Previously, we defined mod routes to import Rust files (hello_route.rs) into routes in the main.rs. But, we haven't done that yet.
So we will write it with some explanation. It will be similar to this.
// It is equal to use
// let hello = path!("hello" / String);
// in main.rs file.
use warp::{
filters::BoxedFilter,
Filter,
};
// 1. "hello"
fn path_prefix() -> BoxedFilter<()> {
warp::path("hello")
.boxed()
}
// 2. / String
pub fn hello() -> BoxedFilter<(String, )> {
warp::get() // 3.
.and(path_prefix()) // 4.
.and(warp::path::param::<String>()) // 5.
.boxed()
}
1. First, we extract hello prefix part to make it reusable.
This is not so meaningful here. But, it will be useful when you want to use prefixes for REST api like api/post/v1, api/user/v1 etc.
You may think of what are BoxedFilter<()> and .boxed() here. Think of them as a glue to help you chain together other methods when they are used inside and API of Warp later in 2.
2. This will be the payload of this file. You define what you want to filter (use) of the user requests here.
3. Only receive (filter) GET requests.
4. This starts with /hello prefix we did before.
5. "Warp, accept param with String type only."
You can see that it will be the argument of the function we will make in route_handler later.
You can also use custom types such as Post, NewPost.
The code used here is self-explanatory. But, I hope these explanations have helped you.
4. Build Handlers to respond to them
In the previous section, we defined the route to filter user requests. We'll define how to handle them by creating hello_handler.rs
It will be similar to this:
use warp;
pub async fn hello(name: String) -> Result<impl warp::Reply, warp::Rejection> {
let reply = format!("Hello, {}!", name);
// println!("{}", &reply);
Ok(warp::reply::html(reply))
}
The code used here is very simple.
First, the return type of handler functions will almost always be this:
Result<impl warp::Reply, warp::Rejection>
Therefore, just copy and paste after reading the documentation for Reply and Rejection.
You must be careful for how to handle Rejection parts. But, it is not necessary for this simple example.
You can also see that the String parameter we filtered in hello_route.rs has became the function parameter here.
Compare it with what we used before in the first part.
.map(|name| format!("Hello, {}!", name));
It is true that Rust Closures are much simpler to use and prototype your Warp app. Use them first in main.rs. When you want to moduarlize it, separate them to routes and handlers.
5. Link them to end the API with macro
With routes/ and handlers/, your application logic is already complete. Set hello! macro inside api/hello.rs similar to this to link them:
#[macro_export]
macro_rules! hello {
() => {
hello_route::hello()
.and_then(hello_handler::hello)
}
}
With #[macro_export], the macros defined this way become globally available at crate level..
They will only work when the module it is including is included at main.rs or lib.rs that represent your Rust project with mod api.
You can use the macros from api inside main.rs or lib.rs without using this.
use crate::{
hello,
};
If you want to use it in other parts of your project as we see in tests/hello_test.rs, you must include the code above for it to work.
All the files to compile your first Warp projects are ready.
First, use $cargo c to verify whether or not it will compile. Then, use cargo run --release for production or cargo run to compile fast.
I hope you can make it!
6. Test it work with CURL and Rust
If you followed the previous parts well, your console should have already shown these after the project compiled.
Rust Warp Server ready at 0.0.0.0:8000
Use $curl 0.0.0.0:8000/hello/www.steadylearner.com to test the end point.
First, use $curl 0.0.0.0:8000/hello/www.steadylearner.com. It will show the same result you can see in the first part.
CURL is very useful for testing end points, but there are problems.
1. First, we have to get the Warp server ready all the time. The Rust compiler will take up your time.
2. Then, you should find out which CURL commands to use. This can be complicated.
Therefore, we'll do Rust test functions to simplify and automate the process. We'll build tests/hello_test.rs equivalent to the CURL command we used.
use warp::Filter;
use crate::{
handlers::hello_handler,
routes::hello_route,
hello,
};
// $cargo test -- --nocapture if you want to use println! etc.
// or test just one function each time.
// For example, $cargo test hello and it passes.
#[cfg(test)]
mod tests {
use super::*;
// 1.
#[tokio::test]
async fn hello() {
let res = warp::test::request() // 2.
.method("GET")
.path("/hello/www.steadylearner.com")
.reply(&hello!()) // 3.
.await;
// 4.
assert_eq!(res.status(), 200, "Should return 200 OK.");
// 5.
println!("{:#?}", res.body());
}
}
The important points are these:
1. We already learned that Warp uses tokio to handle async computations. Thus, the test runner will be also #[tokio:test].
2. Build a specific client request to test before .reply. It won't be difficult to find out what they do because the same thoughts are used again.
3. Define how you will reply to the request here. We already made hello! API for this.
4. See the server return OK with it.
5. Test the function with $cargo test hello.
You can also use -- --nocapture flag to show stdout parts. For example, test it again with $cargo test hello -- --nocapture.
With CURL and a test with Rust, you can verify your API easily, whenever you modify files in routes/ and handlers/.
If you're curious, you can also test performance with loadtest.
That won't be slow and its memory usage is very low.(4.62 Mb in my Linux Ubuntu 18.04 machine.)
Test on your own with this command on Linux while your Warp server is ready in another console.
- To see all memory usages in your system, use this:
$ps -eo size,pid,user,command --sort -size | awk '{ hr=$1/1024; printf("%13.2f Mb ",hr) } { for ( x=4 ; x<=NF ; x++ ) { printf("%s ",$x) } print "" }' | cut -d "" -f2 | cut -d "-" -f1
- You can also include grep and Linux pipe | to see only how much memory Warp needs.
$ps -eo size,pid,user,command --sort -size | awk '{ hr=$1/1024; printf("%13.2f Mb ",hr) } { for ( x=4 ; x<=NF ; x++ ) { printf("%s ",$x) } print "" }' | cut -d "" -f2 | cut -d "-" -f1 | grep "hello_world"
Consult this to compare it with other web frameworks you use.
7. Conclusion
I hope you made it all work and managed to learn how to use Warp with this post. If you want to learn more about it, you can find more Warp examples.
The post was about Warp, but I hope you have also learned how to build a web app with Rust .
Web development with Rust is not mainstream yet. It wasn't easy to invest time in it again and write to help others, although it is much faster than most other web frameworks.
Stay on top of the latest Steadylearner content: follow me on GitHub or here.
You can also star Rust Full Stack.
If you need to hire a developer, you can contact me.
Share with others to help them find the repository better and start programming Rust easily.
Top comments (0)