make sure to use npm make sure to use node v18
package.json
->engines
->node
- specify the correct version to be used for development and production (local and in CI).package.json
->devDependencies
->@types/node
- the major and minor versions should correspond tonode -v
major and minor versions.
- Run
npm install
- Create a .env file based on .env.example. in both the client and server folders.
- Setup the DB by installing postgres.
- Execute the following
npm run migrate:run
from the/server
folder. - Start the app in development mode.
npm run dev
or start the app for productionnpm start
(note when starting the app for production, make sure to runnpm run build
).
- Getting started
- TypeScript
- Express
- React
- React Router Dom
- React Query
- TailwindCSS
- i18next
- Vite, Vitest, Eslint, Husky, Storybook
- Express
- TypeOrm
- Zod
Starts the dev server.
Build the dist/ folder for the client and server app for production.
Start the build production server.
Run tests through Vitest.
Lint files.
Fix all auto-fixable ESLint problems.
Will run all pending migrations.
Will revert the last applied migration.
Will create a new migration file in the migrations folder.
This command generates the ./client/src/i18n/generatedLocales/{locale}.json
files.
Don't manually add translations to ./client/src/i18n/generatedLocales/{locale}.json
files!
Gives you a list of unused translation keys.
Open StoryBook documentation.
The source of truth for the translations is this sheet: https://docs.google.com/spreadsheets/d/1PWfDNHxkEBRknzssW5T7ihaiXDQFndJtxBr8AbWUxcU/edit#gid=0
- Run
npm run translate
and the translations will be generated.
NOTE 1: if you have conflicts in the code and they are all regarding translations files, just rerun
npm run translate
, as the source of truth is always sheet.
How to remove translations?
Do not remove them right away.
- Mark the translation as @deprecated in the translation sheet. (put the string "@deprecated" somewhere in the translation sheet).
- Remove all places in the code where the translation was used.
- After the code was merged to
develop
, only then would be a good time to delete the translations from the sheet.
Why don't delete translations right away?
The front project has type-safety (the strings inside t
are type-safe. t("this_string_is_type_safe")
)
When working with a group of people or different branches, that can cause issues with TypeScript complaining that translations are missing, and the project cannot be compiled successfully.
This project uses StoryBook to document components.
To view it, run npm run storybook
.
To create a story file, make sure to add the .stories.ts
extension.
See Button.stories.tsx
for an example.
This project uses TailwindCSS.
All configuration is in tailwind.config.cjs
.
NOTE: if you use https://github.com/tailwindlabs/tailwindcss-intellisense/ plugin. you can specify the following setting, to improve the DX experience:
{
"tailwindCSS.experimental.classRegex": [
["classNames\\(([^)]*)\\)", "'([^']*)'"]
]
}
Configures Vite to support TypeScript path aliases adds support for SVGR to enable the use of SVG's as in a Create React App, add's "vite-plugin-checker" that spans a new process to run type checking. Setups HMR for Express server for local development.
ESLint is used to lint and format the code.
NOTE: Please make sure to disable other code format tools such as Prettier or Rome.
Husky is used to:
- verify that all test pass before the code gets committed.
- verify that all code is formatted according to the ESLint rules before the code gets committed.
- validate the format of a commit message:
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
See the list of valid "type" types in package.json > commitlint > type-enum
.
Example of valid messages:
fix(FUN-112): Use Big.js in order to have precise rounding
fix: Use Big.js in order to have precise rounding
Vitest is used to create and run unit tests.
To create a test file, make sure to add the .test.ts
extension.
Run npm run test
, to run the unit tests.
npm
is the default package manager.
VS code debug configuration:
{
"name": "ts-node",
"type": "node",
"request": "launch",
"runtimeArgs": ["-r", "ts-node/register", "-r", "tsconfig-paths/register"],
"args": ["${workspaceRoot}/server/src/index.ts"],
"env": { "TS_NODE_PROJECT": "server/tsconfig.json" },
"resolveSourceMapLocations": [
"${workspaceFolder}/**",
"!**/node_modules/**"
],
"skipFiles": [
"**/node_modules/**",
"<node_internals>/**"
]
}
├── README.md
├── dist
│ ├── client
│ ├── server
│ ├── shared
│ └── This folder is generated when `npm run build` is run.
├── client
│ ├── public
│ │ └── Contains public assets.
│ └── src
│ ├── assets
│ ├── components
│ │ └── Contains reusable components.
│ ├── context
│ │ └── Contains contexts components.
│ ├── hooks
│ ├── http
│ ├── i18n
│ │ └── generatedLocales
│ │ └── Contains autogenerated files.
│ ├── layout
│ ├── main.tsx
│ ├── override-types.d.ts
│ ├── pages
│ │ ├── SomePage
│ │ │ └── SomePage.tsx
│ │ └── SomeOtherPage
│ │ ├── components
│ │ │ └── Contains components that are not reusable, but strictly tied to a page.
│ │ └── SomeOtherPage.tsx
│ ├── router
│ └── utils
├── index.html - Vite entry point
├── package.json
├── server
│ └── TODO: document this
├── shared
│ └── protocol.ts
├── tailwind.config.cjs
├── tsconfig.json
├── tsconfig.shared.json - TSconfig options shared by frontend and backend.
└── vite.config.ts
Logging from UseCase
is always done with ctx.logger
. In doing so, we assure that request's data will be embedded in the log.
Otherwise, logging is done directly through LogOutput
.
LogOutput
abstracts the logger from the rest of the application, and enables us to switch the logging backend easily.
ATM, winston
is used for logging.
All logs are routed to stdout
, letting the environment handle them. https://12factor.net/logs.
Dependencies are always specified as exact version (no carets or tildas) to ensure a deterministic build every time.
Updating dependencies is up to the developer - CHANGELOG.md of the dependency should be carefully reviewed and then updated to the appropriate version.
Tip: Use npm-gui
Tip: Use Snyk to evaluate each dependency, for example https://snyk.io/advisor/npm-package/axios . Other packages are easily accessed by updating the package name in the last part of the URL.
- Add the variable to your local
.env
- Add the variable to
.env.example
- Add schema validation in
.env.validate.ts
file. - After this, the TypeScript will allow you to access the variable anywhere like
process.env.VARIABLE_NAME
or inimport.meta.env.VITE_APP_VARIABLE_NAME
. - Be sure to notify your devops to update the
.env
file on development, staging, and production.
- Create a TypeOrm entity in
src/model
(The entity will be automatically detected because ofentities
setting insrc/config/TypeOrmConfig.ts
) - Create a migration file with
npm run migration:create --name=my_migration_name
.
Services are different from UseCase
s:
- services are created for reusability,
UseCase
is never reused - services have no special form/type/interface, they are simple functions, contrary to
UseCase
which is a type of implementation
Important - only when reusability is needed, create a service in src/app/services
, otherwise do the logic in a UseCase
. Creating a service for every feature violates YAGNI and leads to creating proxy UseCase
s which only wrap a service call, in hope that we'll reuse the service someday.
If the service operates with the database, the first argument to all functions is t: TypeOrmEntityManager
which gives the control of the transaction to the function caller.
TypeORM is currently (v0.3.x) having a lot of breaking changes, and is due to have more in the next version (v0.4.x).
Not really an issue, just lack of control and proper documentation for OneToOne and ManyToMany relationships led me to avoid it and develop all relationships as OneToMany/ManyToOne, with a join table as an @Entity in case of @ManyToMany.
Another argument is that OneToOne and ManyToMany relationships don't exist in the relational sense, but foreign keys map to ManyToOne perfectly.
Correct ways of using transactions:
- UseCase transaction (preferred) - use
t
injected to all UseCases as a second argument of the.execute
method. - global connection - import
db
and use it likedb.transaction(t => ...)
Avoid using eager and lazy relations (see here). Lacks control and makes the code more complex - always specify relations to load at query time. Also, set possible for deprecation by the package owner/maintainer.
Do not declare default values of fields in Entity classes because not-selected columns will assume those default values (when partially selecting).
Here is a very nice explanation of this issue. Also, to extend this rule, do not use initializers on neither relations nor properties (columns).
Deprecated
Following are the issues from v0.2
, but should be resolved in currently used v0.3
. They are kept here, just in case.
When using .findOne()
or .findOneOrFail()
, be sure to pass in the id like this .findOneOrFail({where: {id: userId}})
instead of like this .findOneOrFail(userId)
.
Using the latter (wrong) format causes the TypeORM to pull the first entity from the table if the id provided is null
or undefined
, the logic being that null
or undefined
means "apply no filters, just pull one entity".
When using .softDelete
be sure to pass in just the id userRepo.softDelete(user.id)
rather than the whole entity userRepo.softDelete(user)
.
Passing in the whole entity causes TypeORM to generate an UPDATE query with one WHERE clause for each property of the entity, meaning that if any property changed the entity would not be updated (soft-deleted). Deletion should be done strictly by entity's id (primary key).
TypeScript -- a story so big it deserves a file of its own.
Resources:
- TypeScript Handbook
- TypeScript Do's and Don'ts
- O'Reilly - Effective TypeScript
A lot of rules are enforced by tsconfig.json
, as well as .eslintrc
(linter tool) -- we rely on those mainly to ensure code consistency and, to some degree, prevent programming errors.
As enums are poorly implemented in TypeScript (see this discussion for example), use string unions to represent types like this:
export type Pet = typeof Pets[number]
export const Pets = ['CAT', 'DOG', 'PARROT', 'TURTLE'] as const
export const isPet = (value: unknown): value is Pet => Pets.includes(value as Pet)
The above gives us both type and a runtime array of all values, without duplicating the code.
Avoid creating modules who's importing leads to side effects (e.g. initializing objects or connections on module's top level), rather export functions which do said behavior, therefore giving the caller the control of when the module's logic will be executed.