Tramvai – a framework for creating web applications
![](https://habrastorage.org/getpro/habr/upload_files/551/d12/4fb/551d124fbbd4363b9031da939beebc6f.png)
August 30, 2021 on GitHub first release passed framework source code Tramvai. At the same time, the framework began its history much earlier and for a long time was an internal development of the company.
Tramvai is designed for building universal (SSR) React applications along with Next.js, Remix and SvelteKit. The framework serves as the basis for dozens of applications and solves the problems of our developers with the help of more than 150 libraries and modules developed specifically for tramvai applications.
In this article I will tell you about the principles of the framework, its advantages and main capabilities. For those who want to try out the framework right away, a basic application template is available on Codesandbox.
Dependency Injection
A key feature of the framework is the use of the dependency injection principle. Dependency Injection is surprisingly well suited for creating SSR applications and allows you to make even the largest applications flexible and modular.
DI is an integral part of many frameworks in other programming languages. Angular and Nest.js are the most prominent examples of such frameworks in the front-end ecosystem.
The server and client code of SSR applications share many abstractions that operate differently on both platforms. An example of such an abstraction is cookies. DI allows you to create a common service interface for working with cookies and add two different implementations for the server and browser. Thanks to the common interface, such a service can be used in any application code without thinking about the current environment. One use case is any React application component:
For SSR applications, it is important to consider that on the server, the same application processes many requests. Singleton objects such as LRU caches for HTTP clients, the fastify web server itself and its plugins are created only once and persisted in the main DI container of the application.
In the user request handler, for each request from Root DI, a nested DI containerand will contain all entities unique to the user:
request and response objects;
a snapshot of the data that needs to be transferred from the server to the client for correct hydration;
instances of services such as CookieService.
![Visual representation of this hierarchy of DI containers Visual representation of this hierarchy of DI containers](https://habrastorage.org/getpro/habr/upload_files/ee3/808/086/ee380808649194b7f532b308ddc246e2.png)
Lifecycle management — an important component of any framework. For a web application, this cycle includes starting the server, initializing the application, processing requests from users, routing, requesting data, and rendering the page.
In the case of SSR applications, their life cycle also appears in the browser: application initialization, page hydration, SPA transitions.
Tramvai solves the lifecycle problem gracefully and efficiently using its own pattern implementation **CommandLineRunner**, which is inspired by the framework Spring Boot.
The idea is that there are predefined sequential steps for an application on both the server and the client, and together they form a line. You can add any number of actions to each stage.
Actions at each stage are performed in parallel, so we can easily group many API requests into one stage and execute the line for each request as quickly as possible.
![Simplified visualization of the CommandLineRunner for a custom request on the server Simplified visualization of the CommandLineRunner for a custom request on the server](https://habrastorage.org/getpro/habr/upload_files/2d7/04a/e36/2d704ae36a934401cd097255dbbf1cc1.png)
For client SPA transitions, it is worth considering the CommandLineRunner line in conjunction with visualization of flow routing: documentation for CommandLineRunner And documentation for the routing module.
Modules — another important concept of Tramvai. Any tramvai application is assembled from a set of modules, each of which is responsible for specific functions. Here is a list of basic modules, the names of which speak for themselves: ServerModule, RenderModule, RouterModule, LogModule, CookieModule.
Modules work as a glue layer between various functions and DI. Using the CookieService example from DI, the CookieModule module will export a unique token with the service interface and two modules: for server and client code. Each of the modules registers a corresponding token implementation in DI, which will be available in the application. In this case, any libraries for working with cookies can be used for implementation.
![Service dependency diagram for working with cookies in different environments Service dependency diagram for working with cookies in different environments](https://habrastorage.org/getpro/habr/upload_files/1d2/218/cd2/1d2218cd25bc11f45d83d5ea7b2f0665.png)
Application developers can create their modules by isolating closely related functions, and, if necessary, easily move them into separate, reusable packages.
![Modules can connect any other modules, lining up in a common dependency tree Modules can connect any other modules, lining up in a common dependency tree](https://habrastorage.org/getpro/habr/upload_files/7c5/765/893/7c57658937d0d90a93f8f6c98ef4848a.png)
The last concept we will cover in this section is – action games. Actions are the main mechanism for executing requests and generally any side effects in an application. You can register both global actions that will be executed on all pages of the application, and unique ones for specific pages.
The peculiarity of actions is that they limit execution time on the server side. These limits allow HTML to be served to the user as early as possible. In this case, each action that did not have time to execute on the server will be relaunched on the client. For example, if three actions are registered for a page and the API request in one of them is stuck and exceeds the limits, only this single action will be restarted on the client.
![Scheme of how Actions work on the server and client Scheme of how Actions work on the server and client](https://habrastorage.org/getpro/habr/upload_files/5b4/be1/b2c/5b4be1b2cc2638687cc0aef50e95d26e.png)
Tramway capabilities
Creating React applications. They provide a flexible mechanism for building page layouts, hooks for working with DI, actions, routing and global state, and support hot reloading of components without losing local state using Fast Refresh.
Full-stack framework, which provides API routes. These routes allow you to create a full-fledged backend, or Backend for Frontend (BFF), for grouping or transforming requests to the main API.
Microfrontend solution tightly integrated with the application with support for SSR, DI and Module Federation – Child Apps. Child Apps allow you to build application pages from independent blocks with your own release cycle and a separate development team.
Thanks to DI support and the reuse of CommandLineRunner, the development of Child Apps is practically no different from the development of tram applications.
Own routing library, which allows you to flexibly set routes manually, receive routes dynamically and generate them at the application initialization stage based on components from the file system.
State management. Also used own solution with an API similar to Redux in conjunction with redux-act, but with support for independent atomic stores, which significantly improves the performance of store subscriptions via useSelector and useStore.
Library integrated react-query, which effectively solves the issues of caching and deduplication of server data. For any React application, react-query makes it possible to refuse caching data in the global state and reduces the amount of accompanying boilerplate code. There are a number of other benefits: deduplication, optimistic updates, different caching strategies, and much more.
For unit and integration testing libraries have been developed with abstractions as close as possible to the application components, using Jest, Testing-library and Playwright.
To ensure high-quality and uninterrupted operation of tramvai applications, we provide modules with the following capabilities:
handlers for uncaught exceptions;
server health metrics for Prometheus;
sending errors at Sentry on client and server;
Health checks and graceful shutdown for successful deployments;
automatic logging of request errors;
Request Limiter;
DNS caching.
Documentation, tools, examples
A console utility has been created to build applications @tramvai/cli. In addition to development and production assemblies, the CLI is responsible for generating new applications or application components, several assembly analysis modes, updating and installing tramvai dependencies.
The assembly is carried out using Webpack and Babel, and we have also implemented full support for the next generation assembler and transpiler SWC.
Project documentation is located on the website tramvai.dev. We monitor the quality and relevance of documentation; a large community of tramvai application developers within T-Bank helps us improve TT.
To get acquainted with the framework, we recommend going through tutorial on how to create a Pokedex app.
You can learn more about the internal structure and basic concepts in the section Concepts.
The easiest way to launch a tramvai application is This is a basic template on Codesandbox.
A more complex example − todo applicationdeveloped using the feature-sliced methodology.
You can see the results of tramvai applications at https://www.tbank.ru/.
Conclusion
We are glad to have the opportunity to present our development to the front-end community and thanks to feedback we will be able to improve every aspect of the framework.
Our goals are maximum comfort in development and efficient and fast running web applications.
The Tramvai source code is open to contributors, and maintainers are ready to provide assistance on any issues that are not covered in our documentation.
Welcome!