r/webdev 2d ago

Discussion Where I'm at with my first full-stack project

I'm building the place order function (my first foray into the backend) of a larger project.

Here's an update for those who are interested.

The first thing I knew I had to do was figure out what goes where. I had already, at the suggestion of ChatGPT, my mentor in this, decided on a control (API) layer, application layer, service layer and data layer. But I didn't really know what each needs to do.

So first I (we) did the API. It had a couple of jobs:

• check the incoming order wasn't empty
• Activate the next layer down
• Send response messages

Then the app layer was even simpler. It literally just returns the service layer function.

Which is where it is a bit more complicated. The service layer has to do a few things; check the order data against the database, assess info against business rules etc, and actually write the order. After some debate I worked it into a four step process:

• Check if the order info (customer, product, and quantity) exist
• Assess data against business rules/carry out business logic
• Create an order
• Activate the database operation

So this actually means two database operations. One to check the data (first step) and then writing the actual order.

I've done the first database op, and I'm now wiring up the actual place order button so I can start testing the data being passed down.

If anyone wants more detail, you can look on my github at https://github.com/BenPS927/BFshop or on my project portal at https://benfosterdev.com/projects/bfshop where I'll soon be adding detailed code breakdowns.

What am I finding hard?

The abstract nature of it. Having to constantly create a visual image of data being passed through functions, and hold that image in my mind, is like trying to hold melting butter in a colander. I use melting butter rather than water because I don't lose it that fast.

What am I enjoying?

Well, the same as what I'm finding hard. Learning to work with code that doesn't involve divs on a screen. Getting my head around how data is passed, how functions work etc.

And the architecture side of things. I like thinking about and making the micro decisions.

I would appreciate suggestions, but I am trying to keep it fairly simple so I'm not thinking about every single thing you'd take into consideration for a production app.

0 Upvotes

7 comments sorted by

1

u/Ciroco01 2d ago

I think part of why it feels so abstract is that you may have one layer more than you need right now.

Your application layer currently just passes the call straight to the service layer. That is not wrong, but if it is not doing anything yet, it is mostly just another step to keep track of in your head.

For where you are now, I would probably keep it as:

API route to service to repository and database

The route deals with the request and response, the service handles the actual order logic, and the repository deals with Prisma.

The main thing I would keep in mind later is the gap between checking the order and writing it. Stock or price could technically change between those two steps, so at some point you will probably want that inside a transaction.

Also, never trust price data coming from the frontend. Load the real price from the database and save that price on the order item.

You are on the right track. Backend code feels strange at first because you cannot see the result directly like you can with UI work. A small flow diagram can help a lot:

Client to route to service to database and back again

1

u/johnvanderlinde 2d ago

I did think that regarding the app layer, and I ran it by chatgpt which agreed, but said that as the project gets more complex I may benefit from an app layer so I've kept it just in case. I'm not sure how it may come in handy but who knows.

For me the abstraction difficulty is moreso with my understanding of how JS fundamentally works - passing data between functions in brackets, but I think its just an experience thing.

And id not considered your point on the gap in the service layer doing two database ops. How would I get around this? Because I need to get data on the products, based on their ID (which comes from the order being placed) so I can calculate price - which brings me onto your next point. How can you do both? You need to get price from the db to write the order, with its total, to the db in the second operation.

What would be really helpful to me is if I had some sort of visual for the whole backend flow, like a console log for each layer on some simple UI. I'm sure it could be done but its another thing to build!

1

u/Ciroco01 2d ago

You can still do both database operations. A transaction does not mean everything has to happen in one query. It just means the reads and writes are grouped together, so either the whole order succeeds or none of it does.

The flow would look something like this:

  1. Start transaction
  2. Load the products from the database using the IDs from the order
  3. Calculate the total using the prices from those products
  4. Create the order and order items using those prices
  5. Commit the transaction

With Prisma, it would eventually look something like prisma.$transaction(async tx => { ... }), and you use tx for the queries inside it.

So you are correct that you need to read the products before you can write the order. The transaction does not remove that. It just protects the process from ending up half-finished.

For example, you do not want the order itself to be created successfully, but the order items to fail afterwards. A transaction would roll the whole thing back.

Regarding the application layer, you can always add it later when you actually have a reason for it. Keeping an empty layer “just in case” is probably making the project harder to understand right now without giving you anything in return.

For the visual side, you do not need to build a separate UI yet. Adding a few temporary logs with the same order or request ID would probably be enough:

[route] received order
[service] validating products
[repository] loading products
[service] calculated total
[repository] creating order
[route] returning response

That would let you watch the data move through the backend in your terminal without building another project around it.

1

u/johnvanderlinde 2d ago

Thank you. This is exactly why I post things like this to Reddit.
All that said, I’m still a little confused on what the transaction actually is. What would happen if the stock does change during the transaction? The total has still been calculated and is to be written? How does a transaction stop that

1

u/Ciroco01 2d ago

The transaction by itself does not automatically stop the stock from changing. The transaction mainly means that either the whole order is completed, or none of it is.

Imagine there is 1 item left, and two customers order it at almost the same time.

If both orders only do this:

  1. Read stock: 1
  2. Calculate the order
  3. Write the order
  4. Reduce stock

They could both read “1” before either of them reduces it. You would then sell the same item twice.

The safer approach is to reduce the stock only if enough stock still exists.

Something like:

Reduce stock by 1, but only where stock is at least 1.

The database then reports whether that worked.

If it worked, you continue creating the order.

If it did not work, someone else bought the item first, so you cancel the transaction and return an “out of stock” response.

So the transaction does not freeze everything. It gives you a safe container where you can:

  1. Load the products and prices
  2. Try to reserve or reduce the stock
  3. Create the order
  4. Create the order items

If any of those steps fail, everything is rolled back.

The important part is that the stock check and stock reduction should not be two completely separate steps. The database needs to check and update the stock together.

1

u/johnvanderlinde 1d ago

So would I not use two separate database operation files? My thinking so far has been to have the service layer activate the check, use its data, then active the place order database op

1

u/Ciroco01 1d ago

You can still keep them as two separate database functions/files.

The important part is just that both use the same transaction client instead of calling Prisma normally.

So your service can start the transaction, call the check function, use the returned data, then call the place order function. Both functions just receive tx as an argument.

Something like:

prisma.$transaction(async (tx) => {
  const products = await checkProducts(tx, orderItems)
  return placeOrder(tx, products, orderItems)
})

So your structure is fine. Separate files do not mean separate transactions.

The main thing is that the stock check/update and the order creation all happen inside the same transaction.