(Read the Swedish version 🇸🇪)
Warning
This repository contains the source code for the project part of my high school project. Now that it is approved, I feel that it has fulfilled its purpose, therefore, I have archived this repo.
-
-
As I am building a webshop, good SEO is necessary. Good SEO is not something a standard SPA offers, so I had to either server render or write raw HTML. Server rendering sounds nicer.
I chose to use nextjs as it is basically the only way to server render React while also using the new server component patterns.
-
Server components are the obvious way to do server rendering and data fetching. I use them as much as I can.
-
-
I like the concept of unidirectional data flow and global state that Redux popularized. But I dislike all the setup, boilerplate, and complexity that comes with Redux.
I chose Zustand because the concept is identical to Redux, but the implementation is much simpler.
-
I chose to use Tanstack Query in the admin panel to manage both data fetching and caching of data.
-
I stumbled upon Nuqs in a GitHub thread when I was looking for information on how to handle URL query params in Nextjs apps, and Nuqs turned out to be the perfect solution. The API is exactly like useState, but the state is automatically synced with URL queries. The repo deserves more stars.
-
-
-
In my experience, Tailwind is by far the easiest way to do styling.
-
Heroicons tend to be my go-to for icons. They may not have the largest selection, but all the icons look good, and they also have outlined versions.
-
If you're already using React and Tailwind, then Shadcn is an obvious choice.
What sets Shadcn/ui apart from other component libraries is that you own the components. If you want to change something about them, you can simply open the component and change it yourself.
-
-
-
-
I chose Hono because it has an API similar to express, but is compatible with the Bun runtime and generally has better performance.
-
I chose drizzle as my ORM because the API is similar to regular SQL.
-
JWT signing and verification for handling authentication.
-
To encrypt the passwords.
-
-
I chose MySQL as my database partly to learn something new and partly because an e-commerce website is full of relationships, so SQL is perfect.
-
-
Most of the time, I don't even use TypeScript correctly 😂, but it's still a huge help to prevent bugs, especially on the backend, where you're not always sure what all functions return.
-
I don't want to spend time and mental energy formatting my code, so I chose to use prettier (although the side effect is that I mash CMD+S after nearly every key press 😂, but I can live with that). I use the Import-sort plugin from trivago and the Tailwind-classname-sort-plugin, they are nice.
I use Eslint simply with the default settings that Nextjs comes with.
-
-
Inputs need to be validated, otherwise users can send all kinds of crap to the backend, which we don't want to allow.
The most popular validation library is probably Zod. The downside with Zod is that the import size is (unnecessarily) large. Valibot can often have an import size that is 10x smaller than Zod. And I prefer Valibot's documentation.
-
I mostly used Postman just to check the form of my JSON, it's very nice to have it on the second screen.
-
I use docker to simplify hosting my Bun backend.
The image is a visualization of the database created with Dbeaver.
These were my requirements for the database:
- Be able to sell products
- Be able to have different brands and categories
- Be able to sell variations of products, like size and color
- Be able to have discounts on some variations of products, but not others
- Be able to highlight a certain variation of a product
- Be able to have unique images for each variation
- Admins should be able to see what everyone has in their shopping carts, even those who are not registered
I decided to expand the whole "product" thing by considering each variation of a product as an article, and then having ads that contain several articles. The ads thus also need to have some kind of "default" article.
-
I use 🔼 Vercel
-
I run my backend code in a 🐳 Docker container with 🚝 Railway
-
Here I use 🚝 Railway again
-
Database: snake_case
-
API Route names: kebab-case
-
JS/TS Code: camelCase
-
Client-Side Storage: camelCase
-
Types and Schema validation: PascalCase
-
Environment variable: SCREAMING_SNAKE_CASE
-
Extra: Database tables should have Tbl as a suffix
I chose these conventions to simplify and streamline the development process while also following best practices. The idea behind them is that I, as a developer, shouldn't have to think about trivial things like naming, and also that I shouldn't have to think, "damn, what's that endpoint called again?".
This project was full of learnings for me. I encountered all sorts of problems, from locking myself out of my own database, to spending hours with a ".Dockerfile", which should have been called "Dockerfile" 😂.
-
Read
This is actually the second time I tried to build this for the first time because it became chaos due to my state management solution not being well thought out. The entire shopping cart was stored in its own component that was relatively far down in the DOM tree, making it very difficult for other components (like the purchase button) to access it. I realized pretty quickly that I should have used (at least) a context around the whole thing. But the whole dev-ex (and thereby my motivation 😂) went to crap before I actually switched it to a context.
When I rebuilt it, I knew from the start that I needed to solve state management in a thoughtful yet simple way. So I chose to try Zustand, and I think it works fine.
-
Read
This is the first project I've used SQL in. When I started building out the backend, I thought it would be fine to write raw SQL. So I chose to create stored procedures, which I would then call in
the code. I quickly realized that was a very bad pattern, because I needed to use parameterized queries (to protect against SQL injections) and then it became like 7 lines of code for a simple CRUD operation (which wasn't even type-safe), and the code became very hard to read.
Then I got the brilliant idea to abstract away those 7 lines into their own function. Then I realized how stupid that actually was; I had created a helper function for each stored procedure to simplify the readability of the code, but in the process, I made it much worse. Relatively simple CRUD operations had their own helper functions that in turn called on stored procedures, which in turn actually performed the CRUD operations in the database. You can't go on like that if you're going to build something maintainable.
So I chose to explore a bit about what alternatives were available. I was between Prisma and Drizzle ORM. Both seemed like competent solutions. However, I accidentally deleted my entire database when I tried to install Prisma (I misunderstood what "database migration" really means 😂), so frustration led me to 🗄️ Drizzle 😂.
I actually think Drizzle suited me better than Prisma. because the API resembles regular SQL code (which I'm trying to become more familiar with).
-
Read
State in the backend is a whole new concept for me, before this project I never even thought about it. The API routes in Next are stateless, in my case, it's a problem because it means that every route will make its own connection to the database. Then I had my database on RDS which had a max connection of 60, and when you have Next in dev-mode, the connections won't disconnect on hot-reloads, so those 60 connections filled up really fast.
Each individual route has its own state, so at first I thought maybe I could take advantage of that by having some kind of internal route that returns the database connection object. But it turned out complex objects (like database connections) couldn't be sent through HTTP :(.
Personally, I think Next should have some built-in solution for this, but at the same time, they'll never do that considering they think you should do pretty much everything in server components.
The solution is to have some kind of "pooling". Prisma has some magical rust layer that helps with that, but I chose Drizzle 💀. Fortunately, you can also have pooling at the database level, I tried to fix it in my AWS RDS panel, but it wouldn't work, so I decided to rebuild my backend with Bun and Hono.
Part of the motivation for that was also that I was starting to dislike file-based routing more and more. I think file-based routing works fine on the frontend, but not on the backend. Part of the motivation to rebuild it was also that Next doesn't have any real middleware solution for backend routes, and I had to have like 10 lines of boiler-plate code in every "admin/" route just to check if the call actually came from an admin.
Technically, the connection isn't really a singleton because I'm using "mysql.createPool". I do it because I encountered some kind of timeout bug where the connection would close after a few hours, but it was impossible to detect that (unless you want to wrap every endpoint in a try-catch, which you don't). mysql.createPool handles such things for me.
-
Read
The first time I built out the admin panel, I thought I would use server components, but that turned out to be a pretty dumb choice. Server components are rendered on the server, when the browser receives them, it caches them. That means that despite the content having changed, the browser will show the cached version and not ask the server for a new one. In practice, this means that you can add an article in admin/articles/add, and then when you come back to admin/articles, the new article won't show. This caching cannot be turned off. The documentation says (comically enough) something like "no".
Because the content on the admin panel is very interactive, it's probably smarter to build out data fetching on the client instead.
I've never used react query before, but here it actually fits perfectly.
-
Read
Bun is a relatively new thing and therefore there are no good no-bullshit guides on hosting it. After a bit of googling, I figured out that I had to stuff it into a docker container. There's some official Dockerfile template on Bun's website, but I chose to use one from an article on Medium because it seemed much simpler.
The next step then was to find a system to host the docker file. AWS has EC2 or Lambda, but the complexity is really high, (I don't really know how it would have worked, but I guess) I would have first needed to do some kind of automation that listens for commits on the GitHub repo, then fetches the docker file and builds a docker image from it, and then hosts it on EC2 or Lambda. That sounds super complicated, I wanted something simpler.
With Render you can just link the GitHub repo and then it just works, and they seemed to support docker, but the cold-starts are brutal (like 1min). Later I found that Railway could also deploy docker (there the cold-starts are totally okay).
-
Read
The "Login" button is something that is dependent on state. If the user is logged in, it should say "view account", if they are not logged in, it should say "login". The state can be initialized on the client with javascript, but if the user is logged in, it will say "login" before the page hydrates. It looks weird, so I initialized the state with a server component, then the client takes over.
The solution is not 100% optimal because it causes an extra rerender, but navigation is a very important part of UX, so it's something you have to deal with.
Railway app has the same problem, but they haven't solved it haha
This project is part of my approved high school project at Haganässkolan, Älmhult (Technology program).
The report is available as a PDF file in this repo, but it's easiest to open it with nbviewer.
This thesis presents the process of creating an e-commerce site, exploring and utilizing modern web technologies within both front-end-end and back-end development. The work includes an overview of relevant JavaScript frameworks, database choices between SQL and NoSQL, and a discussion on the technical decisions made throughout the project. The final outcome is a functioning ecommerce-store, with insights and reflections on the challenges and lessons learned from the project.
Shop page header | Most popular products |
Product page | Search by brand |
Search filters | Shopping cart |
Checkout | Admin - Home |
Admin - Articles | Admin - Edit articles |
Admin - Brands | Admin - Edit brands |
Admin - Categories | Admin - Edit categories |
Admin - Listings | Admin - Edit listings |
Admin - Planned sales | Admin - Edit planned sales |