Flask and React are a great combination to develop modern web apps. They allow for very flexible architectures, that are scalable both in terms of development and in terms of deployment, are testable, and easy to understand and reason about.
In this post, I'll make a quick tour over all the technological stack that underpins our Flask and React application (which is an application to manage identities, but I won't go into its specifics here). It will be a general overview, without going into the implementation details of each part, intended as a showcase for the kind of architecture that these technologies allow.
We use docker to deploy the different pieces. For development we have an environment managed by docker-compose, that consists of about 25 containers. For production, there is an environment managed with puppet, where containers of any type can be added on demand and will be discovered by the load balancer and added to its pool of services.
The different containerized services that make up the application are as follows:
nginx. Used as a reverse HTTP proxy for the Flask microservices, and also to serve static files and webpack bundles.
Flask microservices. Simple restish APIs that talk json with the client side app.
etcd. Used to serve configuration to the different services. The Flask microservices are equipped with a custom config parser that retrieves configuration parameters from etcd.
redis. Server side sessions are kept centrally in redis, and Flask services, on receiving a request, the first thing they do is to check the authn token in the request and retrieve the corresponding session data from redis.
mongodb. We store the application data in mongodb. Different Flask services manage different databases and collections, and in addition there is a central db, read-only for the Flask services, that is updated asynchronously from the particular dbs, and is the ultimate source of truth.
rabbitmq. Certain tasks, such as sending mail or SMS, or updating the central db, are performed asynchronously, by sending them to an AMQP queue.
React / redux.
We use React with redux to develop the client side app.
React allows for a very easy and convenient componentization of the app, that aligns really well with the DOM and with a developer's intuition of a web layout.
Redux is used to manage the client side state. It keeps state data in a central store, and imposes a single, one way information flow. Any DOM event or user interaction are translated into actions, that are dispatched to the central store, and are the only way to modify the state. And when the central state is modified by these actions, the changes are passed down to the components, that re-render into the DOM as needed. Although at times this may feel overkill, having a few hundred lines of js and knowing at every single step what is going on inside the browser, is priceless. Enforcing this information flow really helps producing code that is readable and understandable.
For async actions, such as querying the servers, that are performed outside the information loop described above, we use redux-saga. Redux has the concept of middleware, that can check the actions that arrive at the central store and produce side effects based on them. Sagas are generator functions -that can be suspended on each yield- that are assigned to specific actions, so that the sagas middleware, when it sees those specific actions, can trigger the execution of the corresponding sagas without interrupting the flow.
The actions are simple JSON objects that conform to this proposed standard.
The js stack
To test the js code we use Karma as a test runner. It is configured to use webpack to prepare the sources for the tests, mocha as a real browser driver (to run the tests in firefox, chrome, etc.), and istambul/isparta for code coverage. The tests are written using enzyme, a testing framework for React.
We use very simple, one concern microservices. We have common initialization code for these microservices, that returns a Flask app with a few common facilities used by all Flask services, such as connection to the sessions redis service, to the etcd service, to the specific mongo dbs, a logger, a connection to a statistics service, or a mailer. We have also a couple of common blueprints (in Flask, a blueprint is just a set of views), such as a blueprint to serve error codes.
To deserialize and validate data sent from the client, and serialize data sent to the client, we use marshmallow, a very simple, framework agnostic library for converting complex datatypes to and from native Python datatypes.
The data sent to the client is sent in the form of flux actions, so that the client side app can directly dispatch it to the redux store without bothering to inspect it, or even check whether it represents a server error or not.
Apart from the above, our microservices are very simple Flask apps, consisting on a single specific blueprint with no more than 8 or 10 simplish views, and a set of tests for those views.