A simple Dockerized CRUD app using Postgres, Express, and React that manages a simple join table relationship. Migrations are handled in the API and use db-migrate. The purpose of this project was to refresh my React skills and teach myself how to work with Material-UI. The most important part of this project (and I would very much like feedback on this) are the Design decisions.
Design inspired by this ui.StackExchange post
Priority number 1: enable the of delivery business value
-
Dockerized from the start
To deliver business value the fastest, you should be in a deployable state from day one. This enables the agile practice of iterative development, and allows stakeholders earlier opportunities to give feedback, saving development time. I'm assuming you know how to deploy Docker apps.
-
A single repo instead of 2. Why? 1 PR for everything instead of 1 PR for the API, and then another for the UI
This also makes deployments easier as some changes might be tightly coupled between, the API and UI. This enables you to deliver business value faster, at least in the initial stages.
Take this with a grain of salt, as this can run into scaling issues down the line.
-
Migrations, migrations, migrations (see Pattern: Database per service)
You need migrations. Don't create separate files that you need to run manually. Your application should be as easy to run and deploy as possible. Migrations enable this, and allow you to deliver business value faster.
-
UI React components grouped by features (see File Structure)
Organizing code by features allows you to spend less time trying to figure out all the relationships between code and more time devlivering business value.
As a developer, it is common to spend more time figuring out what the code does rather than actually writing code. - Alexis Mangin
When you work on a large project, it can be a difficult to identity to origin of an issue. As a developer, you might spend valuable time digging through thousands of lines of code until you understand all the relationships. Organizing your code by modules means you start thinking around a concept where you break down your code into related pieces and provide a public interface to use your module. It helps maximize code sharing and reusability in different sections of your application and even on other projects.
See: Why React developers should modularize their applications?
-
API calls should live separately from React components.
React is a declarative, efficient, and flexible JavaScript library for building user interfaces. - What is React
If there's one place in your app where business logic will be present, it would be close to the API calls. You may need to transform the data returned from the API before presenting it. This business logic should be separate from the React component itself.
This business logic could be different from scene to scene (see How to better organize your React applications?). Knowing this, while we want to isolate the business logic and API calls from the components themselves, they should remain in the context of the business feature (or scene). This allows developers to remain in the context of the feature or scene being developed when delivering business value. If a global API service is implemented, it will quickly outgrow to the point where it becomes less readable and maintainable merely due to its size. Keeping the services, including the API, within the context of a given feature or scene limits this overgrowth. This goes hand in with grouping components into by features (see Grouping by Features or Routes). This enables you to deliver business value faster.
-
JSON objects returned from API should mirror their corresponding DB schemas.
This removes ambiguity in dealing with JSON objects that represent database objects, and respect the database schema as the single source of truth for their structure. The less ambiguity, the more certainty, and the more likely you're able to deliver business value quickly.
Since the API calls are in the same file as everything else for a given component (see #5), it's easy to change this structure as needed, as you only need to modify a single file in the UI. The fewer files you need to change when the schema for a DB object changes, the more quickly you'll be able to deliver business value.
-
Most logs are at the debug level
You don't want to crowd out the logs. There is value in delineating between debug, info, warn, etc. Log levels allow you to filter for the logs you care about quickly, enabling you to deliver business value faster.
-
Don't write more tests than necessary
Writing tests take time. Only write tests that directly translate into one of the necessary capabilities of the product from a business perspective, not a technical perspective. This is also why you should organize your components according to features as opposed to component types.
The way I like to think of what is necessary and what isn't is by asking myself: "Does this operation affect what's stored in the database?". The whole point of computing is, after all, to modify the state of your store. Most front-end applications are CRUD (Create, Read Update, Delete) applications. In other words, they're user-friendly interfaces to a database.
For example: I'll write a test that ensures that you can link an
Entity
with aProperty
, as that's a necessary capability of the product, and translates to a change of state of the database. However, it's not necessary to write a test to make sure that a "Link" button changes color when pressed, as that is neither a business-critical function of the application, nor does it translate to a database change.Take this with a grain of salt (I'm not a professional QA Engineer).
-
Use the most popular component libraries that satisfy business requirements.
Unless absolutely necessary (e.g., legal data privacy regulations), don't use custom libraries, as it will be more difficult for you to find support for issues you run into, and therefore making it more difficult for you to deliver business value.
A rule of thumb: the more popular a software library is, the more support it has. The more support it has, the easier it is to find solutions to common problems that you'll run into when trying to develop a feature. These learned lessons are taken into account and implemented in the corresponding library to make it easier to use. Libraries that are easier to use will help you deliver business value faster.
-
Use auto-formatting
Maintainable software enables the quick delivery of business value. Auto-linting takes the effort out of maintaining a readable codebase, making it easy to keep the code readable. Readable code is maintainable code. Maintainable code translates into enabling the delivery of business value quickly.
- docker
- docker-compose
- nvm
Load the version of node used in the project:
nvm use
docker-compose build
docker-compose up
Open browser to http://localhost:5000
Press Ctrl+C
if in the same terminal instance running the app.
Then run the following:
docker-compose down
Credentials: see docker-compose.yml and api/config/database.json (they should match)
Make sure the relevant environment vars in docker-compose.yml and ui/config/api.json match.
Uses the following VS Code plugin:
Name: Prettier - Code formatter Id: esbenp.prettier-vscode Description: VS Code plugin for prettier/prettier Version: 1.8.1 Publisher: Esben Petersen VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode
-
Start database
docker-compose up database
-
Run migrations forward
docker-compose run api run npm migrate
-
Run migrations backwards
docker-compose run api run npm rollback
See api/migrations/sqls (new migrations: create)
-
Install project-wide dev dependencies
npm install --dev
-
Start database and api
docker-compose up database api
-
In a separate terminal, start the UI
cd ui npm start
-
Navigate to http://localhost:3000
- In Links, the table head has weird behavior when scrolling (see open issue: mui/material-ui#6625)