Money Master, a real API example using "Clean Architecture" approach in Typescript
2023-03-13 20:05:18

TL;DR

I’m pleased to present you Money Master, this is a side project created from a template project for building microservices following “Clean Architecture” principles that I shared on a previous blog post.

Money Master, an API for a money management platform

I would like to cover in a serie of blog posts how you can deliver and evolve a small product following software development best practices, part of this journey will be for sharing some knowledge accumulated during recent years working for a few software companies and part is just for my self learning path and motivational purposes.

This time I wanted to experiment how comfortable I feel using the “Clean Arquitecture” approach with “real business requirements” (take this with a grain of salt, this is a side project) so I came up materializing in an application an idea that was going around my head.

Money Master is a web application that would allow to aggregate your personal expenses and incomes from different accounts in one single dashboard. I know that this is not really anything super innovative but I’ve many ideas around my mind for adding some nice features later on, e.g. the ability of getting track of sharing costs between people. Also this project have some great characteristics that I would use for expanding my knowledge in other areas. Think of event-driven architectures for example, I might use an event queue for making decissions based on transactions that come in.

Taking into account that I’m mostly a backend guy, basically great part of my dayly basis is dealing with the terminal, raw data, infrastructure and code, not great ingredients to catch people attention so this time I’ve started developing the frontend and the API in parallel so then would be a little bit easier to show off the impact of some decissions in a visual way.

For now the application is divided in two microservices, one corresponding to the API where you can basically CRUD a couple of entities, transactions and accounts and the frontend that is created using next.js, the famous React framework for building full-stack applications. Both services are deployed in my personal kubernetes cluster hosted in digitalocean.com.

The API has been created following the “Clean Architecture” approach in an attempt to feel how the application evolves and how comfortable I feel following this architectural pattern. At first glance looks like a little bit over engineering using this just for personal projects if you want to follow strictly some of the theorical principles but I really noticed how valuable if you put this in the right context like for example, enterprise services where you need to seed the bases for the evolution of the product, where developers have different styles and also companies need to deal with people rotation and a growing product.

Perhaps initially your application is a perfect match for using MySQL with a few tables but finally you noticed that according to business grow would be interesting to move to a NoSQL approach in order to get the benefits of performance pluses with less operational costs using MongoDB or DynamoDB. Having chosen “Clean Architecture” makes product evolution much more painless. At the end you rewrite just the part corresponding of database access and the rest of your application remains untoucheable, like pull off a plug and plug in a different one. Believe in me, I’ve had to address db platform migrations and I’m able to see the value here.

Not to mention the facilities you have to test an application that is well layered and following SOLID principles, I will try to deep a bit more into this in later posts.

On the other hand I see some difficulties too, for example validations, if you want to use a convenient framework for validating data endpoints like for example I did for this application using @sinclair/typebox then you’re obliged to validate in the business layer as well, just in case you need to get rid of the Fastify framework, bear in mind that with “Clean Architecture” your core should be usable if you want to move this away from an API service, even though this end up being unlikely (I didn’t cover this in Money Master API, basically for time management, data is validated in the http layer and only some special cases that could not be verified there, in the business layer).

See also how different parts of the application were almost equal at the beginning but they start to differ as long as we introduce some real requirements.

core service

1
2
3
browseTransactions: async (filterCriteria: ITransactionFilterCriteria): Promise<ITransaction[]> => {
return await transactionRepository.browseTransactions(filterCriteria)
}

http endpoint controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
export const browseTransactionsPreHandler = async (
request: FastifyRequest<{ Querystring: TransactionQueryType }>,
reply: FastifyReply
) => {
const { next, prev, start, end } = request.query
// can not do this with Typebox schema
if (!!next && !!prev) {
reply.status(400).send({
statusCode: 400,
error: 'Bad Request',
message: '"next" and "prev" are mutually exclusive'
})
}
if (start && !end || end && !start) {
reply.status(400).send({
statusCode: 400,
error: 'Bad Request',
message: '"start" must be used alongside "end"'
})
}
}
export const browseTransactions = (
transactionRepository: ITransactionRepository
) => async function (
request: FastifyRequest<{ Querystring: TransactionQueryType }>,
reply: FastifyReply
) {
const { next, prev, start, end } = request.query
const filterCriteria: ITransactionFilterCriteria = {}
if (next) {
filterCriteria.pagination = {
next
}
}
if (prev) {
filterCriteria.pagination = {
prev
}
}
if (start && end) {
filterCriteria.start = new Date(start)
filterCriteria.end = new Date(end)
}

const transactions = await transactionService(transactionRepository)
.browseTransactions(filterCriteria)
if (transactions.length) {
return await reply.status(200).send({
next: `${request.routerPath}?next=${transactions[transactions.length - 1].id}`,
transactions
})
}
return await reply.status(200).send({ transactions: [] })
}

repository implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async browseTransactions (filterCriteria?: ITransactionFilterCriteria): Promise<ITransaction[]> {
const criteria: FilterQuery<ITransaction> = {}
if (filterCriteria?.pagination?.next) {
criteria._id = { $lt: filterCriteria?.pagination?.next }
} else if (filterCriteria?.pagination?.prev) {
criteria._id = { $gt: filterCriteria?.pagination?.prev }
}
if (filterCriteria?.start && filterCriteria?.end) {
criteria.valuedAt = {
$gte: filterCriteria?.start,
$lte: filterCriteria?.end
}
}
return await transactionDAO.find(criteria).sort({ _id: -1 }).limit(100)
}

There is still some more work to do in different areas like for example pagination, this is great topic when talking about API design because there are several ways to paginate over a collection and each of them have its benefitst and drawbacks so I’ll cover this topic another time.

Remember if you like this content please support me giving a Star in the following repository

It will encourage me a lot for continue sharing this type of content.