Introduction
Developers often say that there is a huge difference between tutorials and building a real-world project and they are absolutely right. But not for the reason most people think. The code itself doesn't get magically harder but the design choices around it start to matter a lot more.
I recently built a reasonably sized project and as it grew, adding new features become harder than they should be. When my friends tried to help, they found it difficult to navigate the codebase. The problem - there is a working piece of software but it doesn't have any structure.
That experience has taught me a lot of lessons and I think every person starting a maintainable project - with friends or a team can benefit from it.
Lesson 1: Structure your codebase
Before you start implementing features, take some time to structure your project's folder structure and clearly define what each folder is responsible for.
It's easier to think, "I'll just start coding and reorganize things later when it feels necessary.". In practice, it turns out be much harder than it sounds.
For example, in my React Native codebase, I had two folders: helpers _and _utils. Since I never clearly defined what should go into each, I ended up randomly placing files wherever they felt right at the moment. Over time, the distinction between the two became blurry, and navigating the codebase became unnecessarily confusing — not just for others, but for me as well.
Having a well defined stucture early and knowing what belongs where - makes your codebase easier to maintain and grow.
Lesson 2: Document your desing choices
As you build a project, you are constantly making design decisions: how routing works, why a folder exists, why a library was chosen, or why something was done this way instead of another. My mistake was assuming these choices were “obvious” and didn’t need to be written down.
Again in my React Native codebase, I used Expo with file-based routing. Due to that, the routing logic was not explicitly defined in code, it was implicit within the folder structure of the files. At that time, that made perfect sense to me, so I bypassed documenting the route map and how navigation should work.
That turned out to be a bad call.
Others looking at the project were left confused as to why the folders were structured in such a way, or how routing actually worked.
Writing down design decisions-even informal ones-reduces ambiguity. It accelerates the learning process of new contributors, avoids incorrect assumptions, and saves you from explaining the same decisions over and over again. More importantly, it gives you context when in a few weeks/months you revisit that part of the project and wonder why you did things that way in the first place.
Lesson 3: Choose libraries with their limitations in mind
When selecting third-party libraries, don't just ask "Does it work now?" Ask "Will this choice make future changes harder?" A library may solve your problem in the short term, but later you may find it lacks features you really need - or forces you to build those features yourself.
For example, I started with urql as my GraphQL client because it's lightweight, flexible and easy to setup. But as the project grew I ran into places where I wanted to easily refetch or refresh queries after mutations, something which Apollo Client does support out of the box with its refetchQueries and related APIs.
It can still handle these scenarios, but it doesn't bake them into a single built-in API the way Apollo does, instead using its exchange system and additional logic you have to configure yourself. That added complexity slowed me down and made the code harder for teammates to understand.
In other words: the presence or absence of higher-level features — and the ecosystem around a library — can have a huge impact on how easy it is to evolve your project. Always read documentation carefully and try to think a few steps ahead about what features you might need, not just what you need right now.
Lesson 4: Avoid AI when it’s your first serious project
And if this is your first real project-not a tutorial, not a demo, not something you are going to abandon in a week-then my honest advice is: avoid using AI as much as possible.
Yes, it will be harder. Yes, you'll be slower. And that's exactly the point.
Early on, you're not trying to go fast or be productive, you're trying to build intuition. You need to struggle through bugs, misunderstand confusing error messages, and trace broken logic on your own. That's the pain that teaches you how systems actually work. And when AI steps in too early, it quietly removes that learning loop.
Copy-pasting the errors into AI may fix the problem, but it skips the most important step: understanding why the problem happened in the first place. Over time, this creates a false sense of progress — your project moves forward, but your understanding doesn’t.
There's also the issue of context. AI only sees what you give it. Without a full view of your codebase, constraints, and decisions around design, it often suggests isolated fixes. These might work temporarily, but they can introduce inconsistencies or subtle bugs elsewhere-problems you won't even realize were caused by that "quick fix."
Once you've built at least one project the hard way, AI becomes incredibly useful. Then you know what questions to ask, how to evaluate suggestions and when to say no. Before then though and relying on it too much stunts your growth.
Think of it as learning to drive: using autopilot before you understand the controls doesn't make you a better driver, it just hides the mechanics.
Conclusion: Structure beats motivation
If all of this sounds like too much work — planning structure, documenting decisions, thinking deeply about libraries, debugging without shortcuts — that’s okay. Not every project has to be built from scratch with perfect discipline.
If your aim is to get started and move fast, then an opinionated framework can become a huge help.
A framework such as NestJS provides structure out of the box: clear folder organization, conventions for services and controllers, dependency injection patterns, well-defined boundaries. By following those conventions instead of fighting them, you automatically get many of the benefits discussed in this post — with no need to invent everything yourself.
The key takeaway isn’t _“do everything the hard way.”
_ It's "be intentional about the trade-offs you're making."
Whether you design your own structure or adopt one that already exists, what matters is understanding why things are the way they are. That understanding is what makes projects easier to grow, easier to maintain, and easier for others to work on.
Top comments (0)