Actix Rust Tutorial, Part 1
What is Actix Web?
actix-web
is a powerful and pragmatic framework. For all intents and purposes it’s a micro-framework with a few twists.
It’s written in Rust, so definitely this would be one of the fastest frameworks we have today. It runs with speed and utilize every single bit of hardware efficiently.
Honestly, learning Actix is bit difficult than Express, Fastify or Django.
What can I do with Actix Web?
You could develop APIs or build powerful Web Applications. One that handle load of requests without any downtime.
That’s all for theory, let’s dive into some practical.
Points to Cover
- Build a Hello World project.
- Add 2 routes for Get requests.
- Add 2 routes for Post requests.
- Respond with various status codes.
Build a Hello World Project
We are starting from absolute scratch. I would be naming my project hello_actix
.
cargo new hello_actix
Now go to our project directory and install actix-web
.
cd hello_actix && cargo add actix-web
The above command requires cargo-edit to be installed. If you don’t then add the following line to Cargo.toml under [dependencies] in your project root.
actix-web = "3.3.3"
Now open src/main.rs
and you would see some Hello World code but this is not what we want. We want to display Hello World on our Web Browser. Replace all code in main.rs
with following
use actix_web::{web, App, HttpServer};
use std::io;
async fn hello() -> String {
"Hello World".to_string()
}
#[actix_web::main]
async fn main() -> io::Result<()> {
HttpServer::new(|| App::new().route("/", web::get().to(hello)))
.bind("0.0.0.0:8080")?
.run()
.await
}
Let’s see what the above lines are doing. We would start from our main function which returns io::Result<()>
, this is what our HttpServer
would return if we close or stopped our running server. We are also using actix_web::main
macro, this is way to tell Rust run-time system our entry point is here.
Next, we are creating instance of HttpServer
which takes a closure that returns struct instance which implements IntoServiceFactory
. HttpServer
contains all information related to Server itself like port to bind, number of parallel threads, shutdown timeout etc. App
struct's instance in closure contains everything related to logic of our Web Application like routes, database pools, middlewares and guards. We will take a look at each of them in coming parts of tutorial.
route()
method of App
will add new route. It takes 2 arguments - path which is /
(homepage) and Route
which will be returned by web::get().to(hello)
. If you take a close look this structuring would look something like web::<Http Method>().to(<handler function>)
.
At last part of code we invoked bind()
with address, check for errors, called run()
and .await
to wait for server to exit (In our case when we press Ctrl + C).
Now what’s left is our handler function hello()
async fn hello() -> String {
"Hello World".to_string()
}
Generally, we could return variety of Datatypes from handler functions or even asynchronized Streams. Actix expect us to return a Datatype on which Responder
trait is implemented. Responder
trait is by default implemented on couple of Datatypes like String
or Bytes
. Compile and run with cargo run
, if everything goes well then you would see that program outputs nothing to console nor exited. Go to http://localhost:8080/
to get our Hello World.
So here we achieve first milestone of this article.
Add two routes for Get requests
In above section we defined a route for Get request on homepage. You may be able to complete this task by your own. Consider giving a try but in case if you got stuck here is solution. We will add 2 handlers for Get request for 2 paths apart from homepage example at /get_hello1
, /get_hello2
.
/* --- Skipping imports --- */
async fn get_hello1() -> String {
"Get request 1".to_string()
}
async fn get_hello2() -> impl Responder /* use actix_web::Responder */ {
"Get request 2".to_string()
}
/* --- Skipping main function --- */
Remember to import Responder
use actix_web::Responder;
These are our 2 handler functions but in second one we are returning a struct instance which implements Responder
, which is basically a same thing. As Responder
trait is also implemented for String
datatype.
Now some modifications to our main()
.
/* --- Skipping imports --- */
/* --- Skipping handler functions --- */
#[actix_web::main]
async fn main() -> io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/", web::get().to(hello))
.route("/get_hello1", web::get().to(get_hello1)) // added
.route("/get_hello2", web::get().to(get_hello2)) // added
})
.bind("0.0.0.0:8080")?
.run()
.await
}
As our previous route we added 2 more routes specifying our handler functions. And… Yep! that’s all for this milestone. Run your server and go to http://localhost:8080/get_hello1
and http://localhost:8080/get_hello2
to see our specified text. Let's get to Post requests.
Add two routes for Post requests
Apart from adding two additional routes we would also be using actix_web::HttpResponse
. Which is more verbose and gives us a bit more control over data sent to client. So here are our 2 handlers with simple HttpResponse
usage.
/* --- Skipping imports --- */
async fn post_hello1() -> HttpResponse {
HttpResponse::Ok().body("Post request 1")
}
async fn post_hello2() -> impl Responder {
HttpResponse::Ok().body("Post request 2")
}
/* --- Skipping main function --- */
Consider importing HttpResponse from actix_web crate root.
use actix_web::HttpResponse;
Responder
is also implemented for HttpResponse
. So we could return impl Responder
or HttpRequest
, both would work.
Add 2 handlers in our main()
/* --- Skipping imports --- */
/* --- Skipping handler functions --- */
#[actix_web::main]
async fn main() -> io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/", web::get().to(hello))
.route("/get_hello1", web::get().to(get_hello1))
.route("/get_hello2", web::get().to(get_hello2))
.route("/post_hello1", web::post().to(post_hello1)) // added
.route("/post_hello2", web::post().to(post_hello2)) // added
})
.bind("0.0.0.0:8080")?
.run()
.await
}
To test our Post request routes we could use tools like curl or httpie. I would be using curl.
curl -X POST http://localhost:8080/post_hello1
curl -X POST http://localhost:8080/post_hello2
If both of the above command showed the desired result then congrats! Now the last one.
Respond with various status codes
We been using HttpResponse::Ok()
in last 2 sections, you could get an idea that Ok()
means 200
status code. We could use other similar methods to get commonly used status codes.
Some the methods are HttpResponse::Found()
, HttpResponse::NotFound()
, HttpResponse::TemporaryRedirect()
, HttpResponse::InternalServerError()
and lot others. If you don't understand the above mnemonics then other way is to use actix_web::http::StatusCode::from_u16(<status code>)
Below code is just for demonstration of the 2 methods described above, don’t change anything in your code.
use actix_web::http::{StatusCode};
use actix_web::{HttpResponse, Responder};
/* Method 1 */
async fn handler1() -> impl Responder {
HttpResponse::NotFound().finish()
}/* Method 2 */
async fn handler2() -> impl Responder {
let status_code = StatusCode::from_u16(404).unwrap();
HttpResponse::build(status_code).finish()
/* Or other way */
let status_code = StatusCode::from_u16(404).unwrap();
HttpResponse::new(status_code)
}
You get a grasp of it so now let’s add another route that respond with 500
status code or InternalServerError
. We would add route for path /brawl
.
Firstly as usual our handler function
/* --- Skipping imports --- */
async fn brawl() -> impl Responder {
HttpResponse::InternalServerError().finish()
}
/* --- Skipping main function --- */
Specify this function as handler for route /brawl
in main()
/* --- Skipping imports --- */
/* --- Skipping handler functions --- */
#[actix_web::main]
async fn main() -> io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/", web::get().to(hello))
.route("/get_hello1", web::get().to(get_hello1))
.route("/get_hello2", web::get().to(get_hello2))
.route("/post_hello1", web::post().to(post_hello1))
.route("/post_hello2", web::post().to(post_hello2))
.route("/brawl", web::get().to(brawl)) // added
})
.bind("0.0.0.0:8080")?
.run()
.await
}
Compile and run with cargo run
.
You could go to this path to verify or use curl to check its status code is 500
by following command
curl -X GET http://localhost:8080/brawl -I
Here’s my output
HTTP/1.1 500 Internal Server Error
content-length: 0
date: Wed, 09 Feb 2022 12:33:00 GMT
You could play around with this code like trying to implement Put method or return with NotFound
status. Hopefully this would lay a strong foundation for coming articles on Actix. But one thing I think I should add is...
Good Practices
Hopefully we did something useful but still there are tiny requirements or additions to code that would benefit you.
- When binding to an address on server we need to check
PORT
environmental variable. This is mandatory when you take your code live with most hosting providers. - Currently our server prints nothing to Standard out. We could however print something like
Server live at http://localhost:8080
.
Taking care of the above 2 points here’s our final main()
of this article
#[actix_web::main]
async fn main() -> io::Result<()> {
let port = std::env::var("PORT").unwrap_or("8080".to_string()); // added
let server = HttpServer::new(|| {
App::new()
.route("/", web::get().to(hello))
.route("/get_hello1", web::get().to(get_hello1))
.route("/get_hello2", web::get().to(get_hello2))
.route("/post_hello1", web::post().to(post_hello1))
.route("/post_hello2", web::post().to(post_hello2))
.route("/brawl", web::get().to(brawl))
})
.bind(format!("0.0.0.0:{}", &port).as_str())?
.run(); // assign it to variable
println!("Server live at <http://localhost>:{}", &port); // some verbosity
server.await // and await
}