- TypeScript providing better typing experience
- NestJS used at Wiredcraft
- MongoDB officially supported by NestJS
- mongoose reducing boilerplate, and smaller than TypeORM
- Husky, ESLint, Prettier and pretty-quick to automate development
Apply the four-tier architecture for better maintainability. Nowadays maintainability is most important software product quality for team development, to keep the product growing and changing.
See ARCHTECTURE.md for detail.
To develop and test the product, you need a MongoDB instance listening the 27017
port in local.
You can run it in container, to keep your local env clean:
docker pull mongo:4.4
docker run -it -p 27017:27017 mongo:4.4
To test with production build, use docker-compose
then it will launch necessary service and config for production:
docker-compose build
docker-compose up
As a backend engineer, consider three kinds of characteristic:
- How we design a RESTful interface?
- MicrosoftREST API Guideline is a good reference.
- Idempotency is the key to make RESTful API useful and reliable.
- How we design domain model?
- In domain tier: Represent domain's properties and behaviours.
- In application tier: Represent use cases and exceptions.
- How we persist data in database?
- Estimate size of data, cost to backup, time to restore as much as possible.
- Ensure each online query uses index.
- If we assume high-load access on API, consider to apply CQRS, Event Sourcing or other data management pattern for better performance.
When we have a new requirement like "followers/following" or "friends" feature, consider following requirements:
- How many friends a user can have at most.
- If it's many, we cannot use Embedded Data Models so we need to consider deeper about consistency.
- When a user adds a friend, how long time we can wait until the target's followers have updated.
- If we can wait seconds, event-driven architecture could be better for resiliency.
- Transaction is also an option but it probably reduce our options in operation phase like shading.
When code, The 'follow' is User
's behaviour, so implement a follow(User)
method in User
class like:
private readonly friends: Set<UserId>;
private readonly followers: Set<UserId>;
follow(another: User) {
this.follower.add(another.userId);
another.friends.add(this.userId);
}
Then get user's input (DTO) in a controller, and provide it to a method in application service which calls follow(User)
like:
async follow(from: UserId, to: UserId): Promise<void> {
const [fromUser, toUser] = await Promise.all(
this.repo.find(from),
this.repo.find(to)
);
fromUser.follow(toUser);
// TODO: keep consistency
return Promise.all(
repo.update(fromPromise),
repo.update(toPromise)
);
}
MongoDB provides geospatial query support including nearSphere operator, so design the repository API to use this. We also need to consider several points:
- Frequency of search query
- If we need scalability, we can consider to apply the CQRS pattern and use isolated datastore.
- How to sort friends
- Deoends on business requirements, we may sort result by distance,
UserId
or another property.
- Deoends on business requirements, we may sort result by distance,
- Need paging feature or not
- It could be needless if we need just a few friends (e.g. for recomendation).
The geospatial search is not behaviour a use case, so we add a method to application service. It receives a UserId
, distance and limit to search:
async searchNearbyFriends(id: UserId, distance = DEFAULT_DISTANCE, limit = DEFAULT_LIMIT): Promise<User[]> {
if (distance <= 0) {
distance = DEFAULT_DISTANCE;
}
distance = Math.min(limit, MAX_DISTANCE);
if (limit <= 0) {
limit = DEFAULT_LIMIT;
}
limit = Math.min(limit, MAX_LIMIT);
const user = await this.repo.find(id);
return repo.searchNearbyFriends(user, distance, limit);
}
We can encapsule the logic complexity into datastore in this scenario. It makes code simple but we need to evaluate datastore performance and maintainability carefully.