Why Rust & Actix Web for Backends?
milliseconds can make a big difference, routine success and failure when it comes to backend development. Additionally, you do not only want to have performance but also safety that your code runs perfectly and your code does not crash, because it sucks. That's why I'm going to give you a step-by-step guide on how you can build a basic web server using Rust and Actix. So let's go.
Setting Up a Basic Actix Web Server
So first off, I think it's pretty straightforward that we need to install Actix Web so that we can use Actix. Additionally, I've also installed Serde which is basically used for serialization or deserialization when it comes to, for instance, using JSON in your HTTP server. So let's quickly jump into our main.rs file, and then let's just start with our main function here. In that main function, we are just going to use the port 8080 and then we are going to create a simple print line statement which just prints that our server is running on Port 8080. So this should be not really complex so far.
So let's just create our HTTP server, and we import that from Actix Web. After that, we're going to call the new function, and this new function returns some sort of factory of the HTTP server but also requires a closure. In this closure, we now configure our app. So what we could use is just the app from Actix Web, then we say new, and then we can declare, for instance, our services here. Now we can do this later. After that, we're going to make use of the bind function of the HTTP server, and in this bind function, we can define the address which could be Local Host, and we need to define here our port, which basically is 8080. After that, we start the HTTP server with run and then we make use of await. Now, run().await basically runs and waits until the HTTP server was terminated due to maybe some error or due to the fact that we manually terminate this server.
Handling Async Operations and Errors in 'main'
Now this here does not really work because we have some sort of mismatch type. So first off, we have the bind function. The bind function returns a Result, and obviously we need to handle this result. And we can do this for now by just using this question mark operator. Now, this basically means that it catches, kind of automatically, the error and then returns it to the function. So I'm going to make one video specifically about this question mark operator, but for now, it means to you that it just returns the error.
Now, because we now return the error and therefore the Result to our main function, we obviously need to declare the return type here to a Result which comes from std::io, so it's a standard library import, and then we define here the void type in Rust. Additionally, because we use await, we need to make sure that this function is also marked as asynchronous. Now, I'm going to also make a whole series about about the concurrency features of Rust, but for now you can think of async like the typical thing in JavaScript of async/await or promises. In this case, at the moment, we have some weird error that basically says that the main function is not allowed to be async.
And to fix this, we can make use of an Actix macro. So we can define this macro here and can just say actix_web, and then main, and there we go.
Configuring Worker Threads for Concurrency
We have our basic HTTP server, and now we can obviously enhance this by for instance just using the workers method. Now, this workers method needs one argument, and here we can define the threads that this HTTP server should run on. Now, like I said before, threads and async and all that Rust concurrency magic, I'm going to explain this in a different series, but for now, this basically means that our HTTP server can receive multiple requests concurrently and can process them concurrently as well.
Creating a Simple GET Endpoint
So now obviously we have some sort of HTTP server but we do not have an endpoint yet. So how we are going to define our first endpoint? We just declare a function which we can name greet. Now, this basically means that our greet return value should implement the Responder type from Actix Web. We also need to make sure that this is an asynchronous method because, obviously, our function can be called asynchronously in two threads, for instance. And for now, in here, we just return the format of, for instance, "Hello World". Now, format! basically creates a suitable type for our return type we've used here.
Now to make this really an Actix Web endpoint, we obviously need to make sure that this is an endpoint, right? So we need to define the path of this endpoint. For that, we can make use of another macro of Actix Web and here we say get, and we import get obviously from Actix Web again. You can by the way also use just actix_web and then get. And now we can call this function and then we define the path in this function. So for instance, we can declare this endpoint as /greet. So right now, the greet function gets executed whenever a user visits the /greet endpoint. And now we've defined our endpoint, but now we also need to register that into our HTTP server. And we can do this by making use of this service function, and in this service function we define now our greet function, and that's basically it. We've now declared our greet endpoint in our HTTP server.
Working with Path Parameters
Now to run this, we can simply hit cargo run, and this just compiles and builds our server. And now if we visit our Localhost with 8080 and obviously the greet endpoint, we get the return value "Hello World". Now this is pretty cool, but how about path parameters? So, for instance, I want to greet a specific user and not just a simple "Hello World". The only thing we need to manipulate here is the path, obviously. So we define here in curly brackets the name of the path parameter, which in this case is just id, and then we can make use of this path parameter in our greet function by just saying user_id and then defining the type as web::Path and then we define the type of the path parameter. Now obviously we need to import web here from Actix Web, and now we've defined our user_id. And by the way, the naming of this path parameter can be pretty much anything you want, so you could also say instead of user_id, abc. And then after that, I'm just going to quickly say, "Hello" and then user_id in our format! function here.
Now, if we restart our server and then call the endpoint with, for instance, my name, we obviously get first the Rust magic, which basically means cannot pass 'florian' to a u32. So, obviously it cannot pass a string to an unsigned integer. But it can obviously pass 123, for instance, and that works perfectly.
Using Serde for JSON Serialization
So we've now discussed path parameters, but let's just make this whole example a bit more complex. So let's say we want to declare some sort of Rust struct and want to return this in JSON format, but we also want to insert some data through our POST request, for instance. So let's just declare here a simple struct which we call User, and this User struct contains a simple string. Now in here, to really deserialize but also serialize this struct to a JSON format, we can make use of this Serde macro where we make use of derive, and then we say Serialize, but we also say Deserialize. And now that basically means that it can serialize or transform our User struct to a suitable JSON format, but it can also deserialize some input to a User struct.
Managing Shared State with Arc and Mutex
So to simulate a database, I'm just going to make use of a simple map. So for that, I'm going to declare a new type which we call UserDB, and this is now an Arc-Mutex, and now in here we declare our HashMap where the key is a u32 and the value is a User. Now, what exactly does that line mean here? It's pretty simple. We just declare a new type which can be a HashMap. However, we also create through this Arc generic here some Atomic Reference Counting. Now I've already made some sort of similar video about Arc and Mutex, so I highly recommend watching this first, but basically Arc is for moving ownership into closures and managing our references more easily through reference counting. And Mutex basically sets a mutually exclusive flag. Now that means that we cannot manipulate or insert new data into the UserDB in two concurrent threads. Now, obviously, this is not perfect here because the situation can come up that we need to manipulate the data in our DB into threads, so for that, I highly recommend using some sort of data structure that works perfectly concurrently or basically use some real database.
Okay, enough talking, let's just declare our UserDB here by declaring a new variable which we just call user_db and the type is obviously UserDB, and the value is this here. And this looks pretty complex, but it basically suits the UserDB type. Now, it's necessary to declare this DB outside of our closure because multiple threads or multiple endpoints can make use of this DB. But now, obviously, we need to make use of this DB inside our endpoints, right? So we can declare the move keyword here. Just FYI, I also did a video about that, so I highly recommend checking this one out as well. And then we can declare our app_data, which basically will be some sort of web::Data that is provided through Actix Web. So we can make use of web::Data and then new, and then we are going to clone our user_db. Now, it's important to note that clone comes here from our Arc type. So this is needed for basic reference counting. And then we are going to find our app_data here through the app_data function. And now we've basically provided all our endpoints we've declared through this .service() function with our app data, which is a pretty basic DB. Now the order is not important here, so we could also say that the app data is after the service. Actix web basically handles the order here for us.
Implementing a POST Endpoint to Create Data
So let's create a quick POST endpoint. Now, we're going to name this create_user, and obviously this also returns an implementation of the Responder trait. In here we basically define our user data which comes from the request body of this specific endpoint so we need to say web::Json and then we say User as the structure. Now because we use Serde here, Actix Web natively supports this transformation to basically deserializing our request body to our User struct. But then we also need to create or insert this data into our local DB. So we can just make use of the DB directly inside of our function argument. So in here, we just make use of the web::Data again, which is really important, and Actix Web basically handles the passing for us. And here again, the order of the arguments is not important.
Now, obviously, before I forget that, we obviously need to make sure that this is a real post endpoint. And here we just say /users. So we want to make a POST request towards the /users endpoint. Now in here, we basically lock first our mutex in our DB by just creating a new mutable variable db, and then we are locking the function parameter, so our mutex in our DB, and then we say just unwrap. Obviously, it is recommended to handle this error, but for now, just for demonstration purposes, I'm going to unwrap this directly. Then we need to create some sort of new ID because we obviously want to have this automatically. And we say here db.keys() to get all the keys, then we make use of the max function, then we say unwrap_or and then we say &0 and then we say + 1. Now, this reference to zero is necessary because the unwrap_or function needs some sort of reference to a value, so we just say here reference zero. After that, we're going to insert our data into our HashMap. So we now have here the HashMap, and here we basically use the new ID as a key, and then we say user_data.into_inner() as the value.
Now, into_inner is something similar to an unwrapping function which basically unwraps the JSON request body retrieved through calling this API endpoint, and then it just deserializes the JSON data to our user struct and inserts it into our hashmap. And by the way, if we want to return the created data directly in this endpoint, we can do this. But first, we have to make sure that we get the name of the user data. Here we just say user_data.name and then we say clone. In this case, we clone it directly to create a new variable, to create a new reference of this name, and basically clone it into the memory so we can make use of it even after this into_inner function. And now in here, we return an HTTP response, which again comes from Actix Web, and then we make use of the json function. Now again, this comes from Serde directly, so Actix Web supports native Serde. And then we can create a new user, for instance, and just return here the name.
Now obviously, if you want to go beyond this, we could also create a new struct, and in here we also need to serialize this struct so that we basically can convert this struct into valid JSON. And in here we have an ID which is a u32 and a name which could be a string. And after that, we could basically say instead of User, CreateUserResponse. And now obviously there is a missing field, for instance, ID. So we can say id and then new_id, and then the name stays the same. And then obviously we need to declare the service here with the function name that creates the user or has the POST endpoint.
Implementing a GET Endpoint to Fetch Data
Now that was a lot of code. Let's just quickly test if everything works fine. So for that, we're making use of Postman. And obviously, we declare a POST method here. Then we define the URL with the endpoint /users. And in the body, which is of type JSON, we declare the name, so for instance, user xxxx. So then we hit send, and what we get back is a brand new ID with the correct name. Now, if we do this multiple times, we obviously get a new ID back.
Now let's quickly transform our get method here. So obviously our path has to be /users. So we want to retrieve a user by ID, so we just say /users/{id}. Then we have the user_id. We're going to name this function get_user, for instance, but then we also need the data, so our database. And now this function here returns a result of the implementation of the Responder trait or an error. So there obviously could be an error whenever the user ID was not found in our users DB. Now, let's, just like I said before, transform the user ID to into_inner. So user_id instead of being a web::Path type is now a u32. Then again, we can just use the DB where we are going to lock this DB first through the mutex, and then we are just going to unwrap this. And in here we are going to make use of the match keyword and here we're going to say db.get, then we say user_id, which we declare as a reference. Now, obviously this could return some user data, so the ID was found in our DB, and if it was found, we just return Ok. HttpResponse::Ok().json(user_data). And obviously, if like there was no result in our database, so the user ID was not found, we just return the error with error::ErrorNotFound and then we say "User not found". Oh, one more thing I forgot. We obviously need to import the error from Actix Web here, so we actually made use of the error in our generic result here, so we obviously need to import it. Finally, we obviously need to rename greet in our service declaration here to get_user.
Live Demo and Conclusion
And now we have a fully functional HTTP server made with Rust and Actix Web. Now let's just quickly test this. Obviously, we need to create a new user because all our data gets lost whenever we basically restart our server. We've declared a new user with the ID one. And now if we want to get the user, we just call our users endpoint with the path parameter one as an ID, and now this returns our name. And this works perfectly. And obviously you can enhance the GET method here by just returning the ID and the name for instance, instead of just returning the name. But that was basically it. I hope it was not too confusing. I know this Rust concurrency magic and all this generic macro magic could be pretty confusing sometimes. And obviously there's a lot more to know about Actix Web like middleware.
Next Steps: Dockerizing the Rust Application
But the next step would be to just Dockerize this Rust application so that it can run everywhere. And luckily, there's one command that just Dockerized your whole Rust application. So feel free to watch this video here. Anyway, thank you so much for watching. Have a lovely day, and bye-bye.