Rust web application development
- Transfer
The author of the material, the translation of which we are publishing today, says that his most recent experiment in the field of software projects architecture was the creation of a working web application using only the 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.
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 JWT token(JSON Web 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 the standard code of this file for our application.
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, as long as there are not many stable releases of it.
The framework
The cargo-web tool is a direct dependency
WebAssembly
I decided to use the last option, which requires the use of the “night” assembly of the Rust compiler, but at its best demonstrates the native Wasm-capabilities of Rust.
If we talk about WebAssembly, then in talks 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 by the team
The framework
RootComponent . This component is directly mounted to the
LoginComponent . This component is a descendant of the component.
Appearance of the component LoginComponent
ContentComponent . This component is another descendant of the component
Component ContentComponent
RouterComponent . This component stores all possible routes between components containing content. In addition, it contains the initial state of the application
One of the following key concepts
I am pleased to note that it
Each component implements its own Renderable type , which allows us to include HTML-code directly in the source code on Rust, using the macro html! {} .
The opportunity is wonderful, and, of course, the compiler controls its proper use. Here is the implementation code
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. There
Cap'n Proto Protocol
I decided to use the Cap'n Proto Protocol(instead of something like JSON , MessagePack or CBOR )as a data transfer layer forreasons of speed and compactness. It is worth noting that I did not use the RPC protocol interfacethat 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.
You can see that here we have two different options for a login request.
One is for
UIkit - a compact modular front-end framework for developing fast and powerful web interfaces The
user interface of the application's client side is based on the UIkit framework, its version 3.0.0 will be released in the near future. A specially prepared build.rs scriptautomatically loads all the necessary UIkit dependencies and compiles the resulting stylesheet. This means thatyou can add your own stylesto a single style.scss file, which can be applied throughout the entire application. It is very convenient.
I believe that there are some problems with testing our solution. The fact is that it is very easy to test individual services, but it
By the way, here's an idea for a new project: the end-to-end testing framework written in Rust.
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,
PostgreSQL DBMS and Diesel Project
As the main data storage, I decided to use PostgreSQL DBMS. 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 perfectly meets the needs of our project, as it
To establish the connection between
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 required to set up and run a real server. The unit testing of the backend turned out to be, as expected, a fairly simple exercise; carrying out such tests does not cause any special problems.
The application is very easy to deploy using the Docker image.
Docker
With the help of the command
Here is an 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 be created.
This is a big question. Here is what I can answer for him. From the point of view of servers, I tend to answer “yes”, since the Rust ecosystem, besides
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 should reach the same maturity that server projects have achieved. In particular, this concerns 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?
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 JWT token(JSON Web 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 the standard code of this file 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"
Development of the client part of the application
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, as long as there are not many stable releases of it.
The framework
yew
relies on the cargo-web tool , which is designed to cross-compile code into Wasm. The cargo-web tool is a direct dependency
yew
that simplifies the cross-compilation of Rust code in Wasm. Here are the three main goals of the Wasm compilation available under this tool:asmjs-unknown-emscripten
- uses asm.js via Emscripten.wasm32-unknown-emscripten
- uses WebAssembly via Emscriptenwasm32-unknown-unknown
- uses WebAssembly using native Rust for WebAssembly backend
WebAssembly
I decided to use the last option, which requires the use of the “night” assembly of the Rust compiler, but at its best demonstrates the native Wasm-capabilities of Rust.
If we talk about WebAssembly, then in talks 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 by the team
make frontend
), itcargo-web
performs the cross-compilation of the application in the Wasm and packages it, adding some static materials. Thencargo-web
runs 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) in11.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 framework
yew
has some very interesting features. Among them - support for the architecture of components that are suitable for reuse. This feature has simplified splitting my application into three main components: RootComponent . This component is directly mounted to the
<body>
website tag . It decides which child component should be loaded next. If the JWT token is found when you first log into the page, it tries to update this token by contacting the server side of the application. If this fails, the transition to the component LoginComponent
. LoginComponent . This component is a descendant of the component.
RootComponent
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 component ContentComponent
.Appearance of the component LoginComponent
ContentComponent . This component is another descendant of the component
RootComponent
. 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). It can be accessed throughRootComponent
(if the application was launched, it was possible to find a valid session token), or throughLoginComponent
(in case of successful authentication). This component communicates with the backend when the user presses the logout button.Component ContentComponent
RouterComponent . This component stores all possible routes between components containing content. In addition, it contains the initial state of the application
loading
anderror
. It is directly connected toRootComponent
. One of the following key concepts
yew
that 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 work with cookies . Services do not store a certain global state; they are created when components are initialized. In addition to servicesyew
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 is yew
no standard router, 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 it
yew
uses API Web Workersto 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 multithreading on Rust. Each component implements its own Renderable type , which allows us to include HTML-code directly in the source code on Rust, using the macro html! {} .
The opportunity is wonderful, and, of course, the compiler controls its proper use. Here is the implementation code
Renderable
in the component LoginComponent
.impl Renderable<LoginComponent> for LoginComponent {
fn view(&self) -> Html<Self> {
html! {
<divclass="uk-card uk-card-default uk-card-body uk-width-1-3@s uk-position-center",>
<formonsubmit="return false",>
<fieldsetclass="uk-fieldset",>
<legendclass="uk-legend",>{"Authentication"}</legend>
<divclass="uk-margin",>
<inputclass="uk-input",
placeholder="Username",
value=&self.username,
oninput=|e|Message::UpdateUsername(e.value), />
</div>
<divclass="uk-margin",>
<inputclass="uk-input",
type="password",
placeholder="Password",
value=&self.password,
oninput=|e|Message::UpdatePassword(e.value), />
</div>
<buttonclass="uk-button uk-button-default",
type="submit",
disabled=self.button_disabled,
onclick=|_|Message::LoginRequest,>{"Login"}</button>
<spanclass="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. There
yew
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.Cap'n Proto Protocol
I decided to use the Cap'n Proto Protocol(instead of something like JSON , MessagePack or CBOR )as a data transfer layer forreasons of speed and compactness. It is worth noting that I did not use the RPC protocol interfacethat 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
LoginComponent
(here, to get a token, the name and password are used), and another one is for 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 - a compact modular front-end framework for developing fast and powerful web interfaces The
user interface of the application's client side is based on the UIkit framework, its version 3.0.0 will be released in the near future. A specially prepared build.rs scriptautomatically loads all the necessary UIkit dependencies and compiles the resulting stylesheet. This means thatyou can add your own stylesto a single style.scss file, which can be applied throughout 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 it
yew
does not provide the developer with a convenient way to test components and agents. Now, in the framework of pure Rust, integration and end-to-end testing of the frontend is not available. Here you could use projects like Cypress or Protractor , but with this approach, the project would have to include a lot of sample JavaScript / TypeScript code, 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:
/ws
- the main resource for WebSocket-communications./
- the main handler that gives access to the static frontend application.
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. Anyway, the backend should be a stateless system, as 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
As the main data storage, I decided to use PostgreSQL DBMS. 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 perfectly meets the needs of our project, as it
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 handlerUpdateSession
foractix-web
Diesel.rs based.impl Handler<UpdateSession> for DatabaseExecutor {
typeResult = Result<Session, Error>;
fnhandle(&mutself, 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 the connection between
actix-web
and Diesel, the project r2d2 is used . 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 required to set up and run a real server. The unit testing of the backend turned out to be, as expected, a fairly simple exercise; carrying out such tests does not cause any special problems.
Project Deployment
The application is very easy to deploy using the Docker image.
Docker
With the help of the command
make deploy
you can create an image that is calledwebapp
and contains statically related executable files of the backend, the current fileConfig.toml
, 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 using the commandmake run
that launches a network enabled container. 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 an 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 be created.
Results Is Rust ready for web production?
This is a big question. Here is what I can answer for him. From the point of view of servers, I tend to answer “yes”, since the Rust ecosystem, besides
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 should reach the same maturity that server projects have achieved. In particular, this concerns 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?