Rust web application development

The author of the material, the translation of which we are publishing today, says that his most recent experiment in the area of ​​software projects architecture was the creation of a working web application using only Rust language and with the least possible use of the template code. In this material, he wants to share with readers what he found out when developing the application and answering the question of whether Rust is ready to use it in various areas of web development.



Project Overview


The project code, which will be discussed here, can be found on GitHub . The client and server parts of the application are located in the same repository, this is done to simplify project maintenance. It should be noted that Cargo will need to compile the frontend and backend applications with different dependencies. Here you can take a look at the running application.

Our project is a simple demonstration of the authentication mechanism. It allows you to log in with the selected username and password (they must be the same).

If the name and password are different, authentication will fail. After successful authentication, the JWT (JSON Web Token) token is stored both on the client side and on the server side. Storing a token on a server in such applications is usually not required, but I did just that for demonstration purposes. This, for example, can be used to find out how many users are logged in. The entire application can be configured using a single Config.toml file, for example, specifying credentials for accessing the database, or the address and port number of the server. Here is what the standard code of this file looks like for our application.

[server] ip = "127.0.0.1" port = "30080" tls = false [log] actix_web = "debug" webapp = "trace" [postgres] host = "127.0.0.1" username = "username" password = "password" database = "database" 

Client application development


To develop the client side of the application, I decided to use yew . This is a modern Rust framework, inspired by Elm, Angular and React. It is designed to create client parts of multi-threaded web applications using WebAssembly (Wasm). At the moment, this project is under active development, while there are not many stable releases of it.

The yew framework relies on the cargo-web tool, which is designed to cross-compile code into Wasm.

The cargo-web tool is a direct yew dependency that simplifies the cross-compilation of the Rust code in Wasm. Here are the three main goals of the Wasm compilation available under this tool:



WebAssembly

I decided to use the latter option, which requires the use of the Rust compiler’s “nightly” build, but at its best it demonstrates the native Wasm-capabilities of Rust.
If we talk about WebAssembly, then in conversations about Rust today it is the hottest topic. At the moment, there is a lot of work involved in cross-compiling Rust into Wasm and integrating it into the Node.js ecosystem (using npm-packages). I decided to implement the project without any JavaScript dependencies.

When you launch the frontend of a web application (in my project this is done with the make frontend command), cargo-web performs a cross-compilation of the application into Wasm and packages it, adding some static materials. Then, cargo-web launches a local web server that allows you to interact with the application for development purposes. Here is what happens in the console when you run the above command:

 > make frontend  Compiling webapp v0.3.0 (file:///home/sascha/webapp.rs)   Finished release [optimized] target(s) in 11.86s   Garbage collecting "app.wasm"...   Processing "app.wasm"...   Finished processing of "app.wasm"! If you need to serve any extra files put them in the 'static' directory in the root of your crate; they will be served alongside your application. You can also put a 'static' directory in your 'src' directory. Your application is being served at '/app.js'. It will be automatically rebuilt if you make any changes in your code. You can access the web server at `http://0.0.0.0:8000`. 

The yew framework has some very interesting features. Among them - support for the architecture of components suitable for reuse. This feature has simplified splitting my application into three main components:

RootComponent . This component is directly mounted to the <body> tag of the website. It decides which child component should be loaded next. If, at the first login to the page, the JWT token is found, it tries to update this token by contacting the server part of the application. If this fails, the transition to the LoginComponent component is LoginComponent .

LoginComponent . This component is a descendant of the RootComponent component, it contains a form with fields for entering credentials. In addition, it interacts with the application backend to create a simple authentication scheme based on checking the user name and password, and, in case of successful authentication, stores the JWT in a cookie. In addition, if the user was able to authenticate, he moves to the ContentComponent component.


Appearance of the component LoginComponent

ContentComponent . This component is another descendant of the RootComponent component. It contains what is displayed on the main page of the application (at the moment it is just a title and a button to logout). Access to it can be obtained through the RootComponent (if the application, at startup, managed to find a valid session token), or through the LoginComponent (in case of successful authentication). This component communicates with the backend when the user presses the logout button.


ContentComponent component

RouterComponent . This component stores all possible routes between components containing content. In addition, it contains the initial state of the application loading and error . It is directly connected to the RootComponent .

One of the following key concepts of yew , which we will discuss right now, is services. They allow you to reuse the same logic in different components. Let's say it can be logging interfaces or means to support the work with cookies . Services do not store a certain global state; they are created when components are initialized. In addition to services, yew supports the concept of agents. They can be used to organize the sharing of data by various components, to maintain the overall state of the application, such as the one needed for the agent responsible for routing. To organize the routing system of our application, covering all the components, our own agent and routing service were implemented here. There yew no standard router in yew , but in the framework repository you can find an example implementation of a router that supports a variety of URL operations.

I am pleased to note that yew uses the Web Workers API to run agents in different threads and uses a local scheduler attached to the thread to solve parallel tasks. This makes it possible to develop browser applications with a high degree of multi-threading on Rust.

Each component implements its own Renderable type, which allows us to include HTML-code directly into the source code on Rust, using the macro html! {} .

The possibility is wonderful, and, of course, the compiler controls its proper use. Here is the Renderable implementation Renderable in the LoginComponent component.

 impl Renderable<LoginComponent> for LoginComponent {   fn view(&self) -> Html<Self> {       html! {           <div class="uk-card uk-card-default uk-card-body uk-width-1-3@s uk-position-center",>               <form onsubmit="return false",>                   <fieldset class="uk-fieldset",>                       <legend class="uk-legend",>{"Authentication"}</legend>                       <div class="uk-margin",>                           <input class="uk-input",                                  placeholder="Username",                                  value=&self.username,                                  oninput=|e| Message::UpdateUsername(e.value), />                       </div>                       <div class="uk-margin",>                           <input class="uk-input",                                  type="password",                                  placeholder="Password",                                  value=&self.password,                                  oninput=|e| Message::UpdatePassword(e.value), />                       </div>                       <button class="uk-button uk-button-default",                               type="submit",                               disabled=self.button_disabled,                               onclick=|_| Message::LoginRequest,>{"Login"}</button>                       <span class="uk-margin-small-left uk-text-warning uk-text-right",>                           {&self.error}                       </span>                   </fieldset>               </form>           </div>       }   } } 

The connection between the front end and the back end is based on WebSocket connections, which are used by each client. The strength of the WebSocket technology is the fact that it is suitable for transmitting binary messages, as well as the fact that the server, if necessary, can send push notifications to clients. In yew there is a standard WebSocket service, but I decided to create its own version for demonstration purposes, mainly because of the “lazy” initialization of connections right inside the service. If the WebSocket service were created during component initialization, I would have to track multiple connections.


Protocol Cap'n Proto

I decided to use the Cap'n Proto protocol (instead of something like JSON , MessagePack or CBOR ) as the data transfer layer of the application for speed and compactness. It is worth noting that I did not use the RPC protocol interface that Cap'n Proto has, since its Rust implementation is not compiled for WebAssembly (due to the Unix dependencies of tokio-rs ). This somewhat complicated the selection of requests and responses of the correct types, but this problem can be solved with the help of a clearly structured API . Here is the Cap'n Proto protocol declaration for the application.

 @0x998efb67a0d7453f; struct Request {   union {       login :union {           credentials :group {               username @0 :Text;               password @1 :Text;           }           token @2 :Text;       }       logout @3 :Text; # The session token   } } struct Response {   union {       login :union {           token @0 :Text;           error @1 :Text;       }       logout: union {           success @2 :Void;           error @3 :Text;       }   } } 

You can see that here we have two different options for a login request.

One is for the LoginComponent (here, to get a token, the name and password are used), and another one for the RootComponent (it is used to update an already existing token). All that is needed for the operation of the protocol is packaged in the protocol service, thanks to which the corresponding capabilities can be conveniently reused in various parts of the frontend.


UIkit is a compact modular front-end framework for developing fast and powerful web interfaces.

The client interface of the application is based on the UIkit framework, its version 3.0.0 will be released in the near future. The specially prepared build.rs script automatically loads all the necessary UIkit dependencies and compiles the resulting stylesheet. This means that you can add your own styles to a single file, style.scss , that can be applied across the entire application. It is very convenient.

Test front testing


I believe that there are some problems with testing our solution. The fact is that it is very easy to test individual services, but yew does not provide a developer with a convenient way to test components and agents. Now, within the framework of pure Rust, integration and end-to-end testing of the frontend is not available. You could use projects like Cypress or Protractor here , but with this approach you would have to include a lot of sample JavaScript / TypeScript code in the project, so I decided to abandon the implementation of such tests.

By the way, here's an idea for a new project: the end-to-end testing framework written in Rust.

Development of the server side of the application


To implement the server side of the application, I chose the actix-web framework. It is a compact, practical and very fast Rust framework based on the actor model . It supports all the necessary technologies, like WebSockets, TLS and HTTP / 2.0 . This framework supports various handlers and resources, but in our application only a couple of main routes were used:


By default, actix-web runs workflows in an amount corresponding to the number of processor cores available on the local computer. This means that if an application has a state, it will have to be safely shared between all threads, but, thanks to Rust's robust parallel computing patterns, this is not a problem. Whatever the case, the backend should be a stateless system, since many of its copies can be deployed in parallel in a cloudy environment (like Kubernetes ). As a result, the data that forms the state of the application should be separated from the backend. For example, they may reside within a separate instance of a Docker container.


PostgreSQL DBMS and Diesel Project

I decided to use PostgreSQL as the main data storage. Why? This choice determined the existence of the remarkable Diesel project, which already supports PostgreSQL and offers a safe and extensible ORM system and query building tool for it. All this fits perfectly with the needs of our project, since actix-web already supports Diesel. As a result, here, to perform CRUD operations with session information in the database, you can use a special language that takes into account the specifics of Rust. Here is an example of an UpdateSession handler for actix-web based on Diesel.rs.

 impl Handler<UpdateSession> for DatabaseExecutor {   type Result = Result<Session, Error>;   fn handle(&mut self, msg: UpdateSession, _: &mut Self::Context) -> Self::Result {       //         debug!("Updating session: {}", msg.old_id);       update(sessions.filter(id.eq(&msg.old_id)))           .set(id.eq(&msg.new_id))           .get_result::<Session>(&self.0.get()?)           .map_err(|_| ServerError::UpdateToken.into())   } } 

To establish a connection between actix-web and Diesel, use the r2d2 project. This means that we have (in addition to the application with its workflows) a shared application state that supports multiple connections to the database as a single pool of connections. This greatly simplifies the serious scaling of the backend, makes this solution flexible. Here you can find the code responsible for creating the server instance.

Backend testing


Integration testing of the backend in our project is performed by running a test server instance and connecting to an already running database. Then you can use the standard WebSocket client (I used tungstenite ) to send data to the server, taking into account the features of the Cap'n Proto protocol, and to compare the results with the expected ones. This testing scheme has performed well. I did not use special actix-web test servers , since much more work is not required to set up and run a real server. The unit testing of the backend turned out to be, as expected, quite simple, there were no special problems with conducting such tests.

Project rollout


The application is very easy to deploy using the Docker image.


Docker

Using the make deploy you can create an image called webapp that contains statically related executable backend files, the current Config.toml file, TLS certificates, and static frontend content. The build of fully statically related executable files in Rust is implemented using a modified Docker image of the rust-musl builder . A complete web application can be tested by using the make run command, which launches a container with network support. The PostgreSQL container, for the system to work, must be launched in parallel with the application container. In general, the process of deploying our system is quite simple; in addition, thanks to the technologies used here, we can talk about its sufficient flexibility, which simplifies its possible adaptation to the needs of an evolving application.

Technologies used in project development


Here is the application dependency diagram.


Technologies used in the development of a web application on Rust

The only component used by both the frontend and the backend is the Rust version of Cap'n Proto, which requires the locally installed Cap'n Proto compiler to create.

Results Is Rust ready for web production?


This is a big question. Here is what I can answer for him. From the server's point of view, I tend to answer “yes”, since the Rust ecosystem, in addition to actix-web , has a very mature HTTP stack and many different frameworks for the rapid development of server APIs and services.

If we talk about the frontend, then here, thanks to the universal attention to the WebAssembly, now there is a huge job. However, projects created in this area must reach the same maturity that server projects have achieved. This is particularly true for API stability and testing capabilities. So now I say no to using Rust in the frontend, but I cannot help but note that it is moving in the right direction.

Dear readers! Do you use Rust in web development?

Source: https://habr.com/ru/post/416933/


All Articles