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.
1. Use monorepo
2. Configure Code Formatting and Static Code Analysis
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):
- Add running linting and formatting on a Git Hook (preferably on pre-commit), so it won’t be possible to commit wrongly formatted changes. You can easily do it with Husky, which will work for anyone without additional configuration.
- Configure editors to use them on save. With VS Code and WebStorm, it’s possible to commit to the repository project configuration that can enforce some behaviors or install plugins. I think it’s best if every developer configures it themselves in their IDEs.
- Run linter as a part of the CI/CD pipeline. If the repository is configured correctly, developers shouldn’t be able to merge pull requests without proper code formatting.
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.
3. Keep All Build/Run Commands in One Place
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:
- Running development versions of each app and all apps at once (in JS, I suggest using concurrently for this purpose)
- Building production versions of each app and all apps at once
- Running tests
- Running linter or code formatter
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.
4. Use OpenAPI/Swagger for REST API
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?
- New developers don’t need to browse through the code to discover all endpoints.
- Front-end developers don’t need to dive into back-end code to understand how to use endpoints.
- When the project returns from being on hold, every piece of documentation is helpful. Mainly back-end because it’s the center of the app’s logic.
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.
5. Write Descriptions in GraphQL
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:
- They are visible in GraphQL Playground. By the way, enabling it on the development server is always good to work a bit like Swagger UI.
- With a properly configured IDE, descriptions should be visible when writing queries and mutations. It should work as-is with the schema-first approach, but with code-first, you may need to generate the schema file first.
6. Use Health Checks
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.
7. Document Shared Components with Storybook
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.
8. Add Unit Tests
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.
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).
9. Write E2E Tests with Gherkin
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:
- Developers won’t need to write use cases. They just develop actions to do them. The analysts can write use cases, and if they know Gherkin syntax (it’s worth spreading the knowledge in a team), it’s just copy-paste. No double-work is required to be done since one use-case description is enough for every purpose.
- It serves as much better documentation and more readable for non-technical people.
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.
10. Improve Security at the Beginning
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:
- Set CORS to prevent unknown clients from using your API. You can do this easily in NodeJS (Express/NestJS) with the cors library.
- Set proper HTTP headers. In NodeJS, you can easily do it with Helmet. Moreover, if your front-end is written in NextJS, they have an excellent tutorial about security headers that you may find here.
- If you have one server that serves back-end and front-end (even SPA), you may want to take advantage of CSRF protection. It can be quickly done with csurf library, which lets us set the cookie with a CSRF token that the front-end can use as a hidden value in forms. If you use Angular on a front-end, it’s done automatically by HttpClient.
- Add rate limiting for requests on public endpoints. Again, there are simple-to-use libraries like express-rate-limit and @nestjs/throttler.
- If you’re using external auth providers, validate tokens with JWKS. This way, you will be sure the proper party has issued that token. In NodeJS, it can be quickly done with jwks-rsa. It can be integrated with Passport (used in NestJS) — works as a secret provider for it.
- SQL injection is always at the top of most common web app attacks. The easiest way to prevent such an attack is to use ORM for database access, like Prisma (I recommend it) or TypeORM. Suppose you would instead not use ORM for performance reasons and want to be sure no one will write code vulnerable for injections. In that case, you may try to use detect-sql-injection from eslint-plugin-security-node (but I’ve never used it in an actual project, so I can’t tell if it will always work correctly).
11. [BONUS TIP!] Configure the Repository
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:
- CI/CD pipelines. Significantly, the pipeline integrated with pull requests will be helpful for developers, and it will be the last guard before the erroneous code is pushed to the main branch.
- Proper pull request configuration. It would help if you disabled the possibility of merging PR before it’s reviewed and before the pipeline completes successfully. Some systems like GitHub may even enforce how a pull’s title and description should look, but in my opinion, it’s primarily helpful in open-source projects.
- Proper branch configuration. You may want, e.g., to disable pushing directly to the main branch without pull requests.
- Integration with project management system (like JIRA). It’s always a good practice to reference the task in a commit or pull request title, but such integration takes it to the next level: it can display commits in a project management system. It’s always an excellent metric for project managers to keep track of how the project is going.
- Integration with messaging apps (like Slack). Let’s be honest: when all the systems used during work send us notifications and emails about anything, many don’t even look at their inbox. This way, they can omit important things like new pull requests. Integrating the repository with messaging app will mention interested people about important things like new pull requests or failed builds.
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.
About the Author