Follow these tips for configuring full-stack apps, from using a monorepo with Nx to setting up code formatting and CI/CD pipelines, ensuring smooth development.
While creating a new app, most developers first think of proper architecture. While it's important to think thoroughly about how an app should be divided into smaller modules, we shouldn't forget about a thing that every developer in the project will feel — it's configuration. And it's good to start a project with proper configuration.
Here, I'll give you some advice on how we configure new full-stack projects at Synergy Codes. We're doing projects in TypeScript, currently mostly with NestJS back-end and React front-end, but you can use most of these tips in any framework or language.
Monorepo is the easiest way to contain small and mid-size full-stack projects. Especially when working in the JavaScript ecosystem, monorepo allows us to easily share code between projects without a need for creating private NPM packages (or any other means like linking, copying files, etc.). Furthermore, it's comfortable for developers to clone just one repository and run everything at once.
For JavaScript/TypeScript, we have various choices for production-ready monorepo solutions. We mainly use Nx, but we also have used Lerna and Yarn Workspaces. Personally, I prefer Nx because it doesn't only provide a monorepo but also plenty of CLI tools that simplify development by, e.g., bootstrapping modules and components. From one side, it works as a layer over CLI tools for NestJS and Angular, but on the other hand, it also provides a similar solution for libraries without CLI like React.
Configuring code formatting should be the first thing you do before giving access to the project to other developers. Every developer in the team can have a different sense of aesthetics, different IDE configurations, and even different operating systems matter. For JavaScript projects, adding ESLint and Prettier is a must. EditorConfig is also nice to have, especially for configuration files. It's vital to do it at the beginning, since changing anything regarding the code formatting configuration may result in massive commits that will quickly cause merge conflicts.
Of course, only adding them won't solve anything. It would be best if you forced developers to use them. There are three ways (and the best would be to use all of them):
If you use Nx, you'll get ESLint, Prettier, and EditorConfig configured! You just need to make it work in Git Hook, IDE, and pipeline.
It's rather obvious. Everything that could be run in a project should be run from one place. Whatever it is (package.json, Makefile) doesn't matter. Just everything should be in one file. This way, developers will see every script that can be run in a project without a need to read documentation every time (of course, assuming that the project has it).
In the case of monorepos it's effortless. Just use package.json in the root directory (or write a Makefile here if you don't stick to the JS ecosystem) and add all scripts that developers will use throughout development. Here are some examples of what might be helpful to include:
It’s worth mentioning that CLI tools like these from Nx, NestJS, or Angular will already offer plenty of ready scripts. However, I always take the most important ones to package.json.
Moreover, in full-stack projects, we nearly always use databases. Don’t make anyone install a database server on their computers. Just configure a script that would run it in a Docker so that everybody will have the same version with the same configuration. Ideally, it would also be the same version with a similar configuration as on the production server.
OpenAPI lets us document REST API in a simple way, and Swagger adds a nice UI to it. When properly configured, it will contain all endpoints, what data they consume and what they are returning. It’s helpful and the simplest to do, yet good back-end documentation. And why documenting the back-end is essential?
Most back-end frameworks have easy-to-use libraries to generate OpenAPI specifications and automatically generate Swagger UI. For example, NestJS has a dedicated package @nestjs/swagger that generates docs based on the app code (methods, annotations, and even JSDocs).
Turning off Swagger in a production build may be good if your API is not public.
In the previous paragraph, we’ve explored how good it is to document API, but OpenAPI is unfortunately exclusive to REST API. But don’t worry, we also have GraphQL covered here, just with a different tool set.
The best part of it is that it’s built-in in GraphQL. Just when you write schema and resolvers, add descriptions. If you’re going with schema-first approach you may use docstrings for this purpose, in code-first approach the description field.
When we have descriptions defined, we can access them, e.g., like this:
While configuring the project, we shouldn’t only think about developers that will work with that app. Someone (or something) will also have to deploy it and monitor if it’s working. Most often, there would be a service on your infrastructure that will do GET requests to a specific address on your page to check if it’s returning status code 200 (e.g., it’s built-in in Kubernetes or Docker). If there is another response, it will take action, like restarting the service or alerting the administrator. It’s called a health check.
Generally, a health check in an app should check every external service on which it’s dependent. Using the database, external API, Redis, message queue, external storage, or anything like this? Create a health check to check the connection with it.
If you’re using NestJS, there is a ready solution for health checks (@nestjs/terminus) that comes bundled with many ready health indicators.
We’ve discussed documenting the back-end a lot, so now it’s time for the front-end. In every front-end project, we can isolate some shared parts like inputs, generic modal windows, etc. The problem with such components is that as long the project goes on, the bigger chances are that someone will write a new implementation of some existing component. The best way to minimize that risk is to use Storybook for shared components.
Storybook is a very universal tool because it can serve as a front-end documentation but also can be used for testing. You can write components without adding them anywhere in the app, just add it to storybook and test here. Thanks to high configurability, you can add controls to customize behavior to show all the features. What’s more, you can even write E2E tests that are clicking through components in Storybook! It’s much better than component unit tests.
By the way, if you’re using Nx and you generate components via its CLI, it will automatically generate Storybook’s stories. You only need to configure them properly to show all features of the component.
I’ve mentioned before two kinds of tests — Unit and E2E. Let’s focus on the first one now. They are the most basic test you can write for the code. It’s worth configuring them initially, so developers won’t have any excuse for not writing them.
In the JavaScript ecosystem, currently, the best testing framework is Jest. It’s easy to configure, easy to use, and works exclusively in a console, so no additional tools are required to install (like Karma needs a browser).
And here some may ask — why do we require unit tests? I would say that the biggest benefit of unit tests is having living documentation. Seeing unit test, we may find out what the author had in its mind while writing a function or a method. Furthermore, in the ideal world, they should cover all edge cases and not obvious things that may not always be visible at the first glance. That’s very useful for further code expansions, code reuse, and refactoring!
By the way, talking about the front-end, there is a discussion about whether we should unit test components or not. In my opinion (in the case of React), we should move most of the logic from components to separate functions and test them without rendering the component. For the interaction of the component itself, E2E tests are better (e.g., E2E in the Storybook mentioned before). And snapshot testing should be used wisely, not with every component (unless you need 100% test coverage, but you should put quality over quantity).
Now let’s discuss the second kind of tests — E2E tests. They are written mostly to mimic how users would use the app. They can work as a great documentation of use cases, but also may quickly discover bugs that may not be detected by unit tests.
For E2E tests, you will find, nearly everywhere, that in the year 2022, the most recommended framework is Cypress. I won’t disagree with this because I also love it, and similarly to Jest it doesn’t need any additional tools. But here, I would strongly recommend adding one particular tool for writing tests — support for Gherkin language via Cucumber.
What’s Gherkin? Generally speaking, it’s a syntax that enables writing test cases in a natural language (and it's not even exclusive to English, but I strongly recommend using it). You may ask why there is such trouble when we can just write traditional use cases. I can give you two sample reasons:
If you’re thinking about how to encourage analysts (or anyone who writes down tasks) to use Gherkin, you may just integrate it into their toolset. For example, there is a Cucumber for Jira that let’s write use cases in Gherkin as a part of Jira task and later export it to the Git repository.
The last point I wanted to talk about is security. While it’s not something that has to be done at the beginning of the project (like linters), it’s worth implementing some measures when there’s time, and the start of the project is most likely the only time you will have for it (since some may say, there’s no business value in it).
A lot can be done here, especially from the back-end perspective. Here are some things (mostly easy and quick) you may do:
Now, the bonus, the eleventh tip. I’ve left it for the end because it’s not always a job for the developer, but there are good chances that you may have something to say about it.
In most cases, the project you create will be hosted by GitHub, Bitbucket, GitLab, Azure DevOps, or their self-hosted counterparts. All those systems allow doing numerous configurations. Administrators are always doing the basic work, like limiting access to certain people, but much more can be done in practice. Some examples of what may be worth configuring:
I hope these tips will be helpful in your following projects. Remember that exemplary project configuration is also vital, like code architecture. The sooner it’s done well, the better. Furthermore, as the last tip, I may recommend you prepare a boilerplate project with all these things done at the start. It’s practical, especially if you need to do this work often as I do as a part of a team specializing in creating PoCs and MVPs.