Blog / Learning
How to Learn Software Engineering by Building Real Projects (Not Tutorials)
Why finishing forty tutorials can leave you unable to build your own to-do app, and the specific, unglamorous method that actually fixes it — pick something real, ship the ugly version, get stuck on purpose.
Here is a scene that plays out constantly: someone has finished a respected 40-hour full-stack course, built the todo app, the weather app, and the clone-of-a-famous-startup app that the course walks you through screen by screen. They can recite what `useEffect` does. And then they sit down to build something of their own — an app to track which of their plants need watering — and freeze, because the course never told them what to do when the tutorial isn't there.
This isn't a discipline problem or an intelligence problem. It's what happens when every decision in your learning has already been made for you. A tutorial is a fully-solved maze with the correct path highlighted; you get real practice moving your finger along a line, and almost none at the thing that actually determines whether you can build software — noticing you're in a maze at all, and picking a direction when nobody is going to check your answer.
Why the tutorial habit is so hard to break
Finishing a tutorial produces two things that feel identical from the inside but aren't: the sensation of learning, and actual learning. You typed real code, it ran, something appeared on the screen — that's a legitimate dopamine hit, and it's honestly earned in the narrow sense that you did follow the instructions correctly. The problem is that the hard part of software engineering was never "can you type the code that's in front of you." It's "can you decide what code needs to exist," and a tutorial answers that question for you before you ever have to.
The tell is what happens the moment the instructions run out. If you can't describe, in your own words and before opening a new tab to search for it, what the next three files you'd create are and roughly what would go in them — you were following, not building. That's not a criticism; it's just useful information about where you actually are.
The "foundations first" trap
A lot of well-meaning advice tells beginners to nail the fundamentals before touching a real project — finish the data structures course, finish the algorithms course, understand Big-O, then go build something. This advice isn't wrong exactly, but it quietly assumes you can learn abstract fundamentals in a vacuum and have them stick until the day you finally need them, months later. Most people can't. A binary search tree explained on its own, disconnected from any problem you personally care about, is forgotten within a week. A binary search tree you reached for because your plant tracker was getting slow with two thousand entries and a linear scan through an array wasn't cutting it anymore — that one sticks, because you built it under real pressure, for a real reason, and you can still describe exactly why you needed it a year later.
This doesn't mean fundamentals don't matter. It means the order matters less than people think, and letting a real project summon the fundamentals you need, one at a time, produces better retention than front-loading them all before you've ever needed a single one.
What "learning by building" is actually asking you to change
It is not "throw away the fundamentals and wing it." Plenty of people try that version, get stuck on something a five-minute tutorial would have explained, and conclude that building-first doesn't work. The actual change is smaller and more specific: instead of learning a whole curriculum and then looking for a project to apply it to, you pick the project first and let it tell you, one wall at a time, exactly what you're missing.
This inverts the usual ratio badly in your favor. A full curriculum teaches you a hundred things, of which maybe fifteen turn out to matter for the thing you actually end up building. Project-first learning teaches you closer to fifteen things — but they're the right fifteen, because you needed every one of them to make something real work, which means you'll actually remember them a year later. Nobody forgets the CORS error that cost them an entire Saturday.
Picking a first project that's actually the right size
This is where most people sabotage themselves before they've written a line of code — either by picking something so small it teaches nothing (another todo app, identical to the tutorial version) or so large it collapses under its own ambition (a full social network, a game engine, a startup idea with six user roles and a payment system). Neither extreme produces the "get stuck on purpose" effect that actually teaches you anything.
A good first real project has three properties worth checking before you commit to it. First, it solves a problem you personally, actually have — not a hypothetical user's problem, yours, this week. That guarantees you'll notice immediately when something is wrong, because you're the one who has to live with it. Second, you can describe "done" for a first version in one sentence, the way the plant tracker below does — if you can't compress it to one sentence, it's not scoped yet, it's still an idea. Third, it has at least one part you genuinely don't know how to build yet. If you already know exactly how to build every piece, you're not learning anything new — you're just producing.
A worked example, not just a method
Say you want to build that plant-watering tracker. Here's what the process looks like in practice, not in the abstract.
Step one isn't "learn a backend framework." It's writing one sentence: "A page where I can add a plant, see how many days since I watered it, and mark it as watered today." That sentence is small enough to be wrong about nothing and specific enough to know when you're done.
Step two is building the ugliest version that could possibly satisfy that sentence — and meaning it. No login system. No database, even: a JavaScript array sitting in memory that resets every time you reload the page is a completely legitimate first version. The only job of version one is proving the core loop works end to end: add a plant, see it, mark it watered, watch the "days since" number change. Everything else is a distraction from finding out if the idea even holds together.
This is usually where the actual learning starts, because this is where you hit your first real wall — the array resets every reload and that's obviously not good enough, so now you have a genuine, motivated reason to learn what `localStorage` is, or what a database is for, instead of an assigned one. The difference matters: a reason you found yourself sticks; a reason a syllabus gave you evaporates the day the course ends.
Step three is going back and fixing exactly the things that broke, in the order they actually bothered you — not the order a course would've taught them in. Maybe persistence first, because losing your plants on refresh is annoying every single time. Maybe styling never, because you don't care and nobody but you will ever see this. That asymmetry — caring a lot about some things and nothing about others — is what separates your judgment from a curriculum's, and building your own judgment is the entire point.
A second example, to show the method isn't just for toy apps
The same shape works on harder projects too. Say the real goal, eventually, is a Discord bot that moderates a community server — a much bigger undertaking than a plant tracker. The one-sentence version-one spec might be: "A bot that deletes a message containing one specific banned word and posts a warning." That's small enough to ship in an evening, using whatever bot framework has the best getting-started docs, with the word list hardcoded as a single array in the source file.
From there, the real project reveals its own roadmap, one wall at a time, the same way the plant tracker did: the word list needs to be editable without redeploying the bot, so now you need some form of persistence. Different servers want different rules, so now you need per-server configuration. Someone abuses the delete-and-warn behavior to spam warnings at another user, so now you need rate limiting. None of that was on a syllabus. All of it came from the project itself, in the order the project actually needed it — which is exactly why it sticks.
What to do when you're stuck for real, not just uncomfortable
There's a difference between productive discomfort — the slightly frustrating feeling of not yet knowing something you're actively working out — and actually being stuck, where an hour has passed and you have less clarity than when you started. Learning to tell these apart is its own skill, and it's one tutorials never teach you because a tutorial never lets you experience real stuck-ness in the first place.
When you're in the first state, keep going — you're doing exactly what this whole method is for. When you're in the second state, the fix usually isn't more effort, it's a smaller question. "How do I build a plant tracker with persistence" is too big a question to search for effectively. "How do I save a JavaScript array to localStorage and read it back on page load" is small enough that the answer exists in one clear place, and you'll find it in minutes instead of hours. Shrinking the question, not powering through on the same phrasing, is the actual unstuck-ing move most people never learn.
Ship it before it feels ready
There is no version of "ready" that arrives on its own. Ready is a feeling, and feelings are terrible deadlines. Public is not a feeling — it's a repository that exists or doesn't, a URL that resolves or doesn't. Push the plant tracker to a public GitHub repo with an honest README the moment the core loop works, even with placeholder styling and a bug you already know about. Write the bug down as an open issue instead of hiding it.
Something changes the moment a project is public that nothing about writing it privately ever produces: it becomes possible for a stranger to open the repo, and that possibility is what makes you write a clearer commit message than you otherwise would have, structure your files a little more sanely, and occasionally get a comment from someone who hit a bug you didn't know existed. None of that happens to code sitting in a folder called `Untitled` on your desktop.
Where an AI assistant actually helps here — and where it doesn't
Using something like Claude to help you build the plant tracker faster is not cheating, and it's not a shortcut around the learning either — it's a shortcut around the part of learning that was never load-bearing, like remembering the exact syntax for a `fetch` call. What has to stay yours is the one-sentence spec, the decision about what "done" means for version one, and the judgment about which wall to climb first. Hand those off and you haven't learned to build software; you've learned to describe software to something that builds it for you, which is a different and much shallower skill.
The fastest way to tell if you actually understand what you shipped: could you explain the plant tracker's "days since watered" logic to someone else without looking at the code? If not, that's the part to sit with, not the part to move past.
The loop, run enough times to compound
Learn exactly enough to take the next concrete step. Build that step. Put it somewhere public, even if "somewhere" is a repo nobody has visited yet. Explain what you figured out to someone else, or just write it down — teaching a thing back is how you find the part you only half-understood. Then pick a slightly harder project than the last one and go again. Run that loop enough times and, at some point without you quite noticing it happen, you stop needing a tutorial to get started on anything at all — you just start, get stuck, and know exactly what question to go ask.
That loop — Learn, Build, Contribute, Teach, Lead — is the whole operating philosophy behind NebuCoders, and it works for the same reason it works for one person and a plant-watering app: it replaces "watch someone else's finished answer" with "get stuck on your own problem," which is the only thing that was ever actually teaching you anything.