📜 ⬆️ ⬇️

A story about how a team of freelancers writes full JavaScript applications in JavaScript

The author of the material, the translation of which we are publishing today, says that the GitHub repository he was working on and several other freelancers received, for various reasons, about 8,200 stars in 3 days. This repository came in first place at HackerNews and GitHub Trending, 20,000 Reddit users voted for it.



This repository reflects the methodology for developing a full-stack application, which this article is devoted to.

Prehistory


I was going to write this material for quite some time. I believe that I don’t find a better moment than this one when our repository is very popular.
')

No. 1 in GitHub Trending

I work in a freelance team. Our projects use React / React Native, NodeJS and GraphQL. This material is intended for those who want to learn about how we develop applications. In addition, it will be useful to those who in the future join our team.

Now I will talk about the basic principles that we use when developing projects.

The simpler the better


“The simpler the better,” this is easier said than done. Most developers are aware that simplicity is an important principle in software development. But this principle is not always easy to follow. If the code is simple, it facilitates project support and simplifies team work on this project. In addition, the observance of this principle helps in working with code that was written, say, six months ago.

Here are the mistakes concerning the principle under consideration, I had to meet:


Many of the ideas that will be discussed below are aimed at making the code base as simple as possible and maintaining it in this state.

Keep similar entities next to each other.


This principle, the “colocation principle”, applies to many parts of the application. This is the structure of the folders in which the client and server codes are stored, this is the storage of the project code in one repository, this is the decision making about which code is in a certain file.

â–ŤRepository


It is recommended to keep the client and server code in the same repository. It's simple. It is not worth complicating that it is not necessary to complicate. With this approach, it is convenient to organize a coordinated team work on the project. I worked on projects that used different repositories to store materials. This is not a catastrophe, but mono-repositories make life much easier.

The project structure of the client part of the application


We write full applications. That is, the client code and server code. The folder structure of a typical client project provides separate directories for components, containers, actions, reducers, and routes.

Actions and reducers are present in those projects that use Redux. I want to do without this library. I am sure that there are quality projects that use the same structure. Some of my projects have separate folders for components and containers. The component folder can contain something like files with code for entities like BlogPost and Profile . The containers folder contains files that BlogPostContainer container and ProfileContainer . The container receives data from the server and sends it to a “stupid” child component whose task is to display this data on the screen.

This is a working structure. It is, at least, homogeneous, and this is very important. This leads to the fact that the developer who joined the project, will quickly understand what is happening in it, and the role that its individual parts play. The disadvantage of this approach, due to which I have recently tried not to use it, is that it forces the programmer to constantly move through the code base. For example, the ProfileContainer and BlogPostContainer have nothing in common, but their files are next to each other and far from those files in which they are really used.

For some time now I have been trying to place files, the contents of which are planned to be shared, in the same folders. This approach to structuring a project is based on grouping files based on the capabilities they implement. Thanks to this approach, you can make your life much easier, for example, if you put the parent component and its “stupid” child component in one folder.

We usually use the routes / screens folders and the components folder. The folder for components usually stores the code for items such as Button or Input . This code can be used on any page of the application. Each folder in the folder for routes is a separate page of the application. At the same time, the files with the code of components and with the code of the application logic related to this route are inside the same folder. And the code of components that are used on several pages falls into the components folder.

Within the route folder, you can create additional folders in which the code responsible for the formation of different parts of the page is grouped. This makes sense in cases where the route is represented by a large amount of code. Here, however, I would like to warn the reader that it is not necessary to create structures from folders with a very large nesting level. This complicates the movement of the project. Deep nested folder structures are one of the signs of excessive project complexity. It should be noted that the use of specialized tools, such as search commands, provides the programmer with convenient tools for working with the project code and for finding what it needs. But the structure of the project files also affects the usability of it.

By structuring the project code, you can group files based not on the route, but on the capabilities of the project implemented by these files. In my case, this approach shows itself perfectly on one-page projects that implement many possibilities on their only page. But it should be noted that grouping project materials by routes is simpler. This approach does not require special mental efforts in order to make decisions about which entities should be placed next to each other, and in order to look for something.

If we go on the way of grouping the code further, then it can be decided that the code of containers and components can be justifiably placed in the same file. And you can go even further - put the code of two components in one file. I suppose you may well be thinking now that recommending such things is downright blasphemy. But in reality, everything is not so bad. In fact, this approach is quite justified. And if you use React hooks, or the generated code (or both), I would recommend this approach to you.

In fact, the question of exactly how to decompose code into files is not of paramount importance. The real question is why it may even be necessary to divide the components into “smart” and “stupid”. What benefits can be derived from such a separation? There are several answers to this question:

  1. Applications built this way are easier to test.
  2. When developing such applications it is easier to use tools like Storybook.
  3. “Stupid” components can be used with many different “smart” components (and vice versa).
  4. Smart components can be used on different platforms (for example, on React and React Native platforms).

All these are real arguments in favor of separating the components into “smart” and “stupid”, but they are far from being applicable to all situations. For example, we often, when creating projects, use the Apollo Client with hooks. In order to test such projects, you can either create the Apollo response mocks or the mock hooks. The same goes for Storybook. If we talk about mixing and sharing "smart" and "stupid" components, then I, in fact, have never seen this in practice. In terms of cross-platform code usage, there was one project in which I was going to do something similar, but did not. It was supposed to be a monerposite Lerna . Nowadays, instead of this approach, it is possible to choose React Native Web.

As a result, it can be said that there is a definite meaning in the separation of components into “smart” and “stupid”. This is an important concept worth knowing. But often you don’t need to worry too much about it, especially considering the recent appearance of React hooks.

The strength of combining the capabilities of “smart” and “stupid” components in one essence lies in the fact that it speeds up the development and in that it simplifies the structure of the code.

Moreover, if such a need arises, a component can always be divided into two separate components - “smart” and “stupid”.

Stylization


We use emotion / styled components to style applications. There is always the temptation to allocate styles in a separate file. I have seen some developers do this. But, after I tried both approaches, I finally did not find the reasons for moving the styles to a separate file. As is the case with many other things we are talking about here, a developer can make his life easier by combining in one file the styles and components to which they belong.

The structure of the project server application


All of the above is true for the structuring of the server-side code for the application. A typical structure that I personally try to avoid may look something like this :

 src │ app.js #     └───api #   Express      └───config #      └───jobs #    agenda.js └───loaders #     └───models #    └───services # - └───subscribers #      └───types #    (d.ts)  Typescript 

We usually use GraphQL in our projects. Therefore, they use files that store models, services, and discriminators. Instead of scattering them in different places of the project, I collect them in one folder. Most often, these files will be shared, and it will be easier to work with them if they are stored in the same folder.

Do not rewrite type definitions many times.


We use in our projects a variety of solutions, one way or another related to data types. These are TypeScript, GraphQL, database schemas, and sometimes MobX. As a result, it may turn out that the types for the same entities are described 3-4 times. Such things should be avoided. We must strive to use tools that automatically generate type descriptions.

On the server for this purpose you can use a combination of TypeORM / Typegoose and TypeGraphQL. This is enough to describe all the types used. TypeORM / Typegoose will allow you to describe the database schema and the corresponding TypeScript types. TypeGraphQL helps in creating GraphQL and TypeScript types.

Here is an example of defining the TypeORM (MongoDB) and TypeGraphQL types in one file:

 import { Field, ObjectType, ID } from 'type-graphql' import { Entity, ObjectIdColumn, ObjectID, Column, CreateDateColumn, UpdateDateColumn, } from 'typeorm' @ObjectType() @Entity() export default class Policy { @Field(type => ID) @ObjectIdColumn() _id: ObjectID @Field() @CreateDateColumn({ type: 'timestamp' }) createdAt: Date @Field({ nullable: true }) @UpdateDateColumn({ type: 'timestamp', nullable: true }) updatedAt?: Date @Field() @Column() name: string @Field() @Column() version: number } 

GraphQL Code Generator can also generate many different types. We use this tool to create TypeScript types on the client, as well as React hooks that make calls to the server.

If you use MobX to manage the state of the application, then you can use automatically a couple of lines of code to get the automatically generated TS types. If you also use GraphQL, then you should take a look at the new package - MST-GQL , which generates the state tree from the GQL schema.

Sharing these tools will save you from rewriting large amounts of code and help you avoid common mistakes.

Other solutions, such as Prisma , Hasura and AWS AppSync , can also help avoid duplicate type declarations. The use of such tools, of course, has its pros and cons. In the projects we create, such tools are not always used, since we need to deploy the code on our own servers of organizations.

Always use automatic code generation, when possible.


If you look at the code that is created without using the above tools to automatically generate the code, it turns out that programmers constantly have to write the same thing. The main advice that I can give about this is that you need to create snippets for everything that you often use. If you often enter the console.log command, create a snippet, like cl , which automatically turns into console.log() . If you do not do this and ask me to help you with the debugging of the code, it will upset me greatly.

There are many packages with snippets, but it's easy to create your own snippets. For example - using the Snippet generator .

Here is the code that allows you to add some of my favorite snippets to VS Code:

 { "Export default": {   "scope": "javascript,typescript,javascriptreact,typescriptreact",   "prefix": "eid",   "body": [     "export { default } from './${TM_DIRECTORY/.*[\\/](.*)$$/$1/}'",     "$2"   ],   "description": "Import and export default in a single line" }, "Filename": {   "prefix": "fn",   "body": ["${TM_FILENAME_BASE}"],   "description": "Print filename" }, "Import emotion styled": {   "prefix": "imes",   "body": ["import styled from '@emotion/styled'"],   "description": "Import Emotion js as styled" }, "Import emotion css only": {   "prefix": "imec",   "body": ["import { css } from '@emotion/styled'"],   "description": "Import Emotion css only" }, "Import emotion styled and css only": {   "prefix": "imesc",   "body": ["import styled, { css } from ''@emotion/styled'"],   "description": "Import Emotion js and css" }, "Styled component": {   "prefix": "sc",   "body": ["const ${1} = styled.${2}`", "  ${3}", "`"],   "description": "Import Emotion js and css" }, "TypeScript React Function Component": {   "prefix": "rfc",   "body": [     "import React from 'react'",     "",     "interface ${1:ComponentName}Props {",     "}",     "",     "const ${1:ComponentName}: React.FC<${1:ComponentName}Props> = props => {",     " return (",     " <div>",     " ${1:ComponentName}",     " </div>",     " )",     "}",     "",     "export default ${1:ComponentName}",     ""   ],   "description": "TypeScript React Function Component" } } 

Save time, in addition to snippets, can help code generators. You can create them yourself. I like to use plop for this.

Angular has its own built-in code generator. Using the command line tools, you can create a new component consisting of 4 files, which contain everything that can be expected to be found in the component. It is a pity that in React there is no such standard opportunity, but something similar can be created independently, using plop. If each new component you create should be represented as a folder containing a file with a component code, a test file and a Storybook file, the generator will help to create all this in one command. This in many cases makes life much easier for the developer. For example, when adding a new feature to the server, it’s enough to run one command at the command line. After that, files of entity, services and recognizers will be automatically created, containing all the necessary basic constructions.

Another strength of the code generators is that they contribute to uniformity in team development. If everyone uses the same plop generator, then the code for all will be very homogeneous.

Automatic code formatting


Code formatting is a simple task, but, unfortunately, it is not always solved correctly. Do not waste time manually aligning code or inserting semicolons in it. Use Prettier to automatically format the code when commits.

Results


In this article I told you about some things that we have learned over the years of work, over the years of trial and error. There are many approaches to structuring the code base of projects. But among them there is no one that can be called the only correct one.

The most important thing I wanted to convey to you is that the programmer should strive for the simplicity of organizing projects, for their homogeneity, for using an understandable structure that is easy to work with. This simplifies team development of projects.

Dear readers! What are your thoughts on ideas for developing a full-stack JavaScript application outlined in this material?



Source: https://habr.com/ru/post/456340/


All Articles