Why My First App Is a Beautiful Disaster

I have an app I’ve been working on for years. It’s the first thing I ever coded from scratch: a March Madness Tournament Simulator. At the time, I thought it was genius. I thought I was a genius.

Looking at it now, it’s a mess.

My past self, who thought he was very smart, wrote something that technically worked but was absolutely miserable to maintain. Every March, I get the itch to refactor it, add tests, build pipelines, and “do it the right way.”

But honestly? Who cares?

No Tests? Oh Boy.

This thing has near-zero unit tests…

I can see exactly what happened. My past self Jimmy-Neutron-brain-blasted through implementation at the speed of thought. Components grew massive. State lived everywhere. Logic sprawled across files like a spilled drink. I could tell the app was working because I knew exactly what I was working on and was testing as I built. Who needs unit tests? I was the test.

Now every change feels like defusing a bomb. Would this tweak fix an issue or collapse the entire app into an unholy pile of runtime errors? No idea. Looked good as I tested it. What are side effects again? Ship it and pray.

Testing wasn’t a priority. Learning paths rarely are neat or complete, and testing wasn’t in my online courses. Testing is big. It’s nuanced. It’s easy to skip, but the consequences show up later when the project grows or when other people get involved. It’s an essential piece of every application. Gotta have tests, right?

Then again, it’s fine. I am the only developer on an app that no one is paying me to build. You don’t learn testing by reading about it. You learn it by shipping without it, breaking things, and realizing that tests are less about confidence and more about containment. You learn by making your components so unmanageable that you have to start to break them down and begin testing them. Brings you back to industry standards real quick. But now you know why tests matter.

March Madness app code coverage is 7.23%...
Could be better.

This variable could be anything…

I wrote this app with JavaScript and a dream.

To be fair to my past self, there’s some impressive stuff in here. Intricate data structures. Real schemas. Non-trivial logic. But once that data started flowing through my app, I had absolutely no idea what I was working with.

Enter: an army of console.logs.

This is why we type data. This is why we define models, schemas, and contracts between layers of the app. Yes, it’s tedious. Yes, it feels unnecessary when everything is still fresh in your head. But try coming back a year later and figuring out how your app is crawling through a conference to find a team’s turnover ratio.

Dynamically typed languages feel amazing until scale, time, and human memory get involved. And suddenly, setting up model after model feels like the best deal you’ve ever been offered. Strict typing is an essential piece of every application. Gotta have models, right?

Then again, it’s not a big deal right now. Debugging is a useful skill. One of the most important. This won’t be the last time you face a hacked-up solution with bad or no type safety. Learn how to dig through the code despite the fog. Learn to use those AI tools. Learn to build meaningful models and apply them as you refactor. Typing begins to explains itself.

// compare picks to winning teams
Object.keys(state[nextRoundKey]).forEach((region) => {
  // Wish I knew what the nextRoundKeys were... 🤔
  state[nextRoundKey][region].forEach((matchup, i) => {
    // What's a matchup??? 😒
    matchup.forEach((teamObj, j) => {
      // What's a teamObj??? 😵‍💫 Bad name too...
      if (
        action.payload.winningTeams.includes(
          action.payload.picks[region][i][j].team
          // What's a pick??? 😵
        )
      ) {
        teamObj.selected = true
        state.playerScore += pointsPerCorrectPick
      } else {
        teamObj.selected = false
      }
    })
  })
})

No Errors. No Problems.

What’s an error boundary? I had no idea.

APIs always work forever, right? My app will never break!

When something went wrong, the app just… died. No tracers. No helpful messages. No graceful recovery. It broke, and I had to dig through the wreckage to figure out why. The user? Completely in the dark.

I didn’t know what I didn’t know. Error handling wasn’t a priority because there were a hundred other things to learn first. There are always more exciting things to learn first, like animations.

This is where my design background should have kicked in. Error states, recovery paths, and user messaging matter, but I was busy making my basketball spin. No time for that. I knew errors mattered, but from the developer side, they felt optional compared to “making it work.” Even if “making it work” involved made-up requirements. It didn’t take long to see how quickly the user experience falls apart when technical debt accumulates. You should NEVER compromise the user experience. You have to have useful errors, right?

Then again, it broke, and who cares? My ten friends and family members who use the app certainly do not. Understanding what makes a great app is a refined instinct. There are certain things apps need that a user expects and make dev life easier. Nothing like watching users tear apart your work. Nothing like getting a generic 500 server error and trying to find what broke. Priceless experience.

March Madness app code coverage is 7.23%...

Well that isn't supposed to happen.

Hello world! Here’s my API keys.

I revisited my database setup after a long hiatus.

And there they were.

My API keys. Just sitting there. Public.

I built this project before I even knew what environment files were. One of the most fundamental concepts in coding. I blew right through that lesson. Anyone could have ripped these and left me a hefty usage bill. Or stolen my highly curated and groomed basketball statistics! How could I have missed something so important?

So yes, laugh. I did.

This is one of the clearest litmus tests for growth. The face-palms. The curses you mutter at your past self. That’s not failure, that’s progress.

Face-palms are crucial. We all suck when we start. It’s part of the deal. The important point is that you leave your code better than you found it. Laugh as you do it.

No better place to collect face palms than a beautiful disaster of a project. Just be sure to protect your app secrets. This one you should care about.

import { initializeApp, getApps, FirebaseApp } from 'firebase/app'
import { getFirestore, Firestore } from 'firebase/firestore'

// Safely stored in their very own .env variables 🎉🎉
const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
  measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
}

const firebase_app: FirebaseApp =
  getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]

const db: Firestore = getFirestore(firebase_app)

export default db

Who Cares

Seriously. Who cares?

You’re learning. You’re the only developer on a solo passion project. Who cares if it sucks? This first app probably isn’t for money, and if it is, you should strongly consider making your own beautiful disaster first.

You don’t have to show you’re an expert. You don’t have to follow industry standards. You don’t have to be perfect. There’s something cathartic about slamming code and watching ideas come to life. It brings joy back to engineering. All while earning those scars the old devs talk about in refinement.

Industry standards matter, but they can drain the joy out of building. Especially if you adopt them without context. Spend your Saturday playing virtual basketball. Not reaching 100% code coverage.

And don’t worry about those tech interviews. They want to learn how you fixed past issues so you can save their own borked code. Dissecting your own mistakes is a strong case that you have what it takes. Test-driven development hits different after you’ve spent a week chasing a bug through an untested codebase. Then adopted its principles.

That’s how it clicks for me. That’s how I prove my skills to tech leads. Not by memorizing best practices, but by learning why they exist. The hard way.

Never Apologize. Just Build.

Be unapologetic about it.

If you’re a junior developer waiting to learn everything before you build, don’t. Your courses about architecture, testing strategies, error handling, or typing won’t save you in tech interviews. These ideas only become real once you’ve felt the pain they’re designed to prevent.

Your first project isn’t supposed to be clean. It’s supposed to expose your blind spots. It’s supposed to show you what breaks when you move fast, skip steps, and trust your intuition too much. That feedback loop is the curriculum.

Then tell how you built something, broke something, investigated something, fixed something, and deployed something. The story is what matters. Not your perfect code.

So build the ugly thing. Ship the brittle thing. Maintain the thing that makes you cringe six months later. Show it off anyway. That discomfort is proof you’re leveling up. It’s part of your story.

Just keep building.