Introduction
I am David, your developer on duty, and this video is an overview on how to create a basic web service in Rust.
This video series is all about Rust because I'm fairly new to it, and I take this as an opportunity to learn more about it. However, I will not cover every aspect, especially the basic syntax and basic language features.
System Architecture Overview
Let me give you a quick outline of what I'm going to do. And I'm using here this open-source tool called Excalidraw, which is awesome to scribble around. I want to create a Rust server, and this Rust server will be able to interact with the outside world through HTTP. And since it's stateless, it will have some database, which is a PostgreSQL database. And it will have one table called users, and I will be able to interact with my database through SQL. I will be able to read my users and insert new users.
Setting Up the Actix Web Server
If you want to build a web server in Rust or any other programming language, you need to choose a framework first. And Actix Web is one of the more popular ones, which also supports asynchronous functions. There's a nice GitHub repository with samples which I cloned here and slightly adapted. And looking at the code, it's quite simple to build a web server.
First, you need to decorate your async main function with actix_web::main, and this is needed to have an async runtime because Rust by default doesn't provide one. Then you create some data which should be available in your handler functions. In this case, it's a Postgres pool which I create here.
Then you create your server using the HttpServer constructor, which takes an App factory, aka a closure which returns an App. And in this App, you plug in the data you want to make available in your handlers, and you create a service. In this case, I have a resource /users where I define two handlers: one for GET requests and the other one for POST requests. Then you need to bind it to your server address and run it and await the server.
Defining the Data Model with Serde
Before we define our handler functions, we first need to create a model. In this case, we just define a public structure called User. It has four fields called email, first name, last name, and username, all of type string. And to retrieve it through HTTP and send it across HTTP, we need to be able to serialize and deserialize this struct. There's a nice little crate called Serde, and the only thing you have to do is to derive Serialize and Deserialize, and that's it.
Implementing HTTP Handlers
Let's define the handler functions to handle the HTTP requests. In this case, get_users to handle the GET request. And we always have access to our app data, in this case, our PostgreSQL database pool. And the result of this function must be a result of HttpResponse and Error. That means we either return an OK HTTP response or an error.
Now we can use the database pool to retrieve a client. And let's just assume for now that we have this method get_users which takes a client to retrieve all our users. So we await this function call to get our users and provide an HTTP response with status OK and with the users as a JSON object.
Now for our POST request, we define our function called add_user, which again takes our database pool, but additionally also needs to extract the user information out of the HTTP request body. And this we can do using a declarative approach. So we can use this web::Json extractor and the User struct to retrieve the user information. We just have to call user.into_inner(), and then we can again retrieve our client from our database pool. And let's just assume we have this add_user function to create a user on the database. So we provide the client and the user information, await the function call, and then return the HttpResponse::Ok() with a new user as JSON data.
Database Logic: Fetching Users
To use our user structure in database interactions, we derive postgres_mapper. And this provides some functions which we later use.
Our database function get_users is constructed as follows: we take our database client and provide a result of our vector of users or our custom error, MyError. First, we create a statement. In this case, it's just a SQL string: SELECT * FROM testing.users. We take our client, call query with our statement and no parameters, await the result, iterate over the result, and then map each row into our User struct and collect it as a vector of users and return Ok(users).
Database Logic: Adding a User
Our add_user database function works like this. It takes again the client and this time also the user info and provides a result of our user or MyError. We create our database statement. This time it's a statement with placeholders. It's an INSERT INTO testing.users with email, first name, last name, and username with values, and it returns the table fields. We replace the table fields with our sql_table_fields function from our user struct. Then we prepare our statement and await it.
Then we take our client and again call query with our statement and now, this time, our user info parameters. We await it, iterate over the result, map each row again to our user struct, collect it into a vector of users, we pop it, and return Ok or our custom error.
Live Demonstration
Now let's run our service. And the prerequisite is a running Postgres database in the background. So I hit cargo run, and the server is running. Now I can trigger some HTTP requests. To get a list of all users, I execute the GET endpoint and you can see I will get the list of all users. Now let's create a new one. And you can see the status 200, which is okay. Now let's trigger the users endpoint again, and you can see our new new user was correctly inserted in our database.
The Problem with Tight Coupling
Let's look at the flaws of this implementation, especially with regard to the domain. So in our Rust server, I created this User struct. And the problem is if I would have changed this user struct, I would also need to change my database table, and I would need to change all the queries to interact with my database. So in the optimal world, I could derive my database table out of this user struct as well as all my queries.
Solution: Procedural Macros & TQL
To overcome this problem, we need to somehow derive our database table as well as our queries based on our user struct. And one technique to achieve that is called procedural macro, which you already know, for example, from Serde to deserialize our struct.
I did some investigation to find out if there already is a crate who provides this procedural macro, and it turns out there is. It's called tql, and it works like this: you define your struct and then you derive(SqlTable). And this lets you create a database table, create an insert statement, an update statement, a delete statement, and also a query statement. I want to build something similar to this and learn more about procedural macros. So it's great that this project exists so I can take some inspiration and get some help if I'm stuck on the way.
Conclusion and What's Next
In the next video, I will create my first procedural macro to learn the basics. Thanks for watching and stay tuned.