Web Application Boring Stack: 2019 Edition
Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.
At JetBridge, we enjoy collaborating with our clients on software to take pride in and pushing the boundaries of our knowledge and expertise. We frequently set up new projects, and so we have come up with a standardized and straightforward set of tools, libraries, and frameworks to speed up the launch of new applications and deliver as much value as possible without repeated effort.
Our setup isn’t perfect, nor it’s the end-all stack for any project. However, it’s something we’ve developed over the years, and it works for us. We keep introducing new tools and techniques, and our workflow is constantly evolving, so take this as more of a current snapshot. If you are reading this in July 2019, keep in mind that at least some parts of the stack may have been modified already.
Our approach to software development is about not overcomplicating things.
We focus on pragmatism and business value, not the latest, the coolest, or the hippest frameworks and tech out there. Like any geek, we love toying with new cool stuff, but we don’t believe in adopting something new just for the sake of it. When it comes to choosing a library or framework to base an application on, maturity and support are considered, as well as maintainability, community, available documentation and its support, and whatever actual value it brings to us and our clients.
Many engineers tend to make their software more complex than it needs to be. They use exotic tools in place of well-known and widely available ones. They try to shoehorn a neat piece of tech featured on Hacker News into something it isn’t really suited for. They depend on unnecessary external services to do something achievable by extending the existing ones. They resort to low-level solutions where abstraction would really simplify things or use something too fancy and complicated where a basic system-level tool or language would be more expedient.
Applied wisely, simplicity can make code much more readable and maintainable, and operational environments easier to manage.
At the time this is written, whatever frameworks and libraries we use have likely been superseded by cool new JS jams, and you may very well sneer at our unfashionable choices. Without further ado, this is what works well for us now:
- React: Vue may have more stars on GitHub but React is still the standard — it is still well-used and supported by Facebook, among others. Writing apps with React hooks really feels closer to functional programming, adding a new level of composability and code reuse. Done with HOCs, like before, that would be too clunky.
- Material-UI for React is a toolkit that has almost every kind of widget or tool you might need, powerful theming and styling options. It integrates CSS-in-JS smoothly and has a solid look out of the box. It is essentially an implementation of the UI paradigms championed by Google — working within its constraints and visual language, you are sure to have a reasonable starting point.
- Create-React-App/react-scripts: It does everything you may need and helps configure your new React app with sane defaults. No need to tinker with Webpack or HMR again. We have extended CRA/r-s to churn out new frontend projects with extra ESlint, prettier options, and Storybook.
- Storybook: We decided to build a library of small and larger components implemented in isolation based on mock data, rather than always code and test layout and design inside the completed app. It frees our UI devs from waiting for the completion of backend endpoints, enforces the concept of reusable and self-contained components, and enables us to easily preview various interface states.
- TypeScript: Everyone uses TypeScript now because it’s good, and you shouldn’t miss out either. It takes some getting used to and learning to use it properly with React, and Redux requires some small amount of learning, too, but it’s entirely worth the effort. Remember: there’s no need to use
any. And when you think there is, what you need is probably just a (generic) type argument.
- ESLint: ESlint works great with TypeScript now! Don’t forget to set
extends: ['plugin:@typescript-eslint/recommended', 'plugin:react/recommended', 'react-app']
- Prettier: Set up your IDE to run Prettier on your code when you hit save. Not only does it enforce a consistent style but also means you can be way lazier about formatting your code and get better formatting with less typing.
- Redux: Redux is nice... Or so we are told. You do need some central place to store your user authentication info and stuff like that, and redux-persist is super handy in such situations. In the spirit of keeping things simple though, ask yourself if you need Redux for what you’re doing. Maybe you do, or maybe a hook or a state will suffice. You may think at first that you want to cache some API response in Redux, but if you start adding server-side filtering, search, or sorting, you are better off having a simple API request inside your component.
- Async/await: Stop using the Promise API! Catch exceptions in your UI components, where you can present the error to the user, not in your API layer.
- Axios: Our HTTP client of choice. We use JWT for authentication, and our axios-jwt interceptor module can take care of token storage, authorization headers, and refreshing.
Nothing crazy or unusual here, and that’s the point—just stick with what’s standard unless you have a good reason not to.
Most projects involve setting up a typical REST API, talking to other services, and performing CRUD on a PostgreSQL DB. Our go-to stack is:
- Python 3.7. Python is clean, readable, has a massive repository of community modules on PyPI, active core development, and a pretty good balance of high-level dynamic features without getting too obtuse or distracting.
- Type annotations and type linting with
mypy. Python does have type annotations, but they are very limited, not well integrated, and not that much useful for catching errors. I hope the situation improves because many errors need to be caught at runtime in Python, in contrast to TypeScript or Go. This is Python’s biggest drawback in my opinion, and we do our best with
- Flask, a lightweight web application framework. Flask is nicely suited to building REST APIs, providing just enough structure to your application for handling WSGI, configuration, database connections, reusable API handlers, tracing/debugging (with AWS
X-Ray), logging, exception handling, authentication, and flexible URL routing. We don’t rely on Flask too much besides using it to hold everything together in a coherent application without imposing too much overhead or boilerplate.
- SQLAlchemy for declarative ORM with nice features for handling of Postgres dialect features such as
JSONB. Mixins for model and query classes are powerful and are something we increasingly have been using for features like soft deletion. Polymorphic subtypes are one of the most interesting SQLAlchemy features, allowing you to define a type discriminator column and instantiate appropriate model subclasses based on its value.
- Testing: subtransactions wrapping each test, pytest-factoryboy for generating fixtures from our model classes for pytest and for generating mock data for development environments. CircleCI. Pytest fixtures. test client.
- Flask-REST-API with Marshmallow helps succinctly define REST endpoints, serialization, and validation with minimal boilerplate, making heavy use of decorators for declarative feel when appropriate. As a bonus, it also generates OpenAPI spec documents and comes with Swagger-UI to automatically provide documentation of every API endpoint and its arguments and response shapes without any extra effort required.
- We are currently developing Flask-CRUD to further reduce boilerplate in the common cases for CRUD APIs and mandating strict data model access control checks.
If required, we can use Heroku or just EC2 for hosting, but all our recent projects have been straightforward enough to be built as serverless applications. You can read about our setup and the benefits it brings us in more detail in this article.
We have built a starter kit that ties together all of our backend pieces together into a powerful template to bootstrap new serverless Flask projects — sls-flask. Give it a try if you think of building a database-backed REST API in Python. It offers a lot of power and flexibility in a small package. There is nothing special or exotic included in it, but we believe the foundation it provides adds up to an extremely streamlined and modern development toolkit.
All our toolkits and templates are open source, and we make it a point to submit often bug reports and fixes to the upstream of the modules that we use. Try out our stack or let us know what you’re using if you’re happy using it. Share and enjoy!
Python isn’t the only possibility for building webapp backends, and we’re also doing some projects in Go, where we can get the benefits of a compiled language and fantastic type safety and compile-time checks. If we can find something simple and powerful like flask-rest-api for Go, we’d certainly like to see how it can improve our setup and when it would be more appropriate. It’s been really excellent for microservices and projects where a lot of higher level patterns aren’t so necessary.
Ruby on Rails is a mature and battle-tested framework with many years of development and improvements behind it and allows for rapid prototyping and can be well-suited to MVP projects.
On iOS our language of choice is naturally Swift; it’s modern, strongly typed, and easy to read even for our Android teammates. The entire iOS platform has an awesome community working on a large array of open source projects in Swift (and Objective-C). We prefer Swift to react-native for apps of any size or complexity.
When writing android apps we also choose tools that are mature, well known and have proven their value in business projects. The Android community is very active and creative, but it is wise to approach new fancy solutions with a dose of reserve. Here’s our stack:
- Dagger: a dependency injection framework with a pretty steep learning curve but it does tremendous work in keeping the project well-organized. Even though it requires some initial setup, it proves its value as the project grows. Unlike most DI frameworks, Dagger doesn’t use reflection; it’s all based on compile-time code generation.
- RxJava:streams for everyone! Reactive extensions allow us to build responsive, message-driven, reliable code. RxJava does everything in terms of multithreading, synchronization, data manipulation and together with Dagger it helps us keep the app’s components decoupled.
- Retrofit: turns your HTTP API into a Kotlin interface, works great with RxJava.
- Android Jetpack, Data Binding: we also make heavy use of Android Jetpack (navigation!) as it plays well with the rest of our stack and solves some fundamental problems. Data binding helps to keep our views always up to date and additionally saves us lots of boilerplate code.
React-native (with expo.io ): For simple apps, react-native with TypeScript is easy and any React developer can just jump in and start developing a mobile app. We’re familiar with the many limitations of react-native, so as the project grows we either start writing some screens totally natively or we plan to start with the native SDK from the very beginning.