DEV Community

Cover image for Why I Rebuilt My First API From Scratch
Chris Kechagias
Chris Kechagias

Posted on

Why I Rebuilt My First API From Scratch

I built my first API two months ago. It worked. I deployed it. Users could hit endpoints, get responses, everything functioned. By most measures, it was a success.

Then I looked at the code with someone who actually knew what they were doing, and realized: working isn't the same as professional.

So I archived it and started over.

The First Version

My original retail inventory API did what it was supposed to do. CRUD operations, PostgreSQL database, running in production on Render. I followed tutorials, copied patterns I found online, debugged until things worked.

The problem wasn't that it failed. The problem was I couldn't explain why it succeeded.

When someone asked me to walk through the architecture, I stumbled. Why did I structure the database this way? "That's what the tutorial showed." What's your error handling strategy? I didn't have one—just try-catch blocks scattered everywhere.

I could make it run, but I couldn't defend the design. That bothered me.

Starting Over

The easy path: refactor incrementally. Add tests here, clean up there, gradually improve. But the foundation was shaky. I'd built on patterns I didn't understand. Every new feature would compound that.

Starting from scratch felt wasteful. Two months of work, gone. But those two months taught me what questions to ask. The second time, I wasn't following tutorials blindly—I was making actual decisions.

What Changed

Database Design

Version one: single products table with basic fields. Version two: proper product-variant relationship.

Products have category (Tees, Sweaters, Pants), name, color, price, and collection ("FW24", "SS25"). Variants add size (S-M, L-XL, One Size) with individual quantity and in_stock tracking.

This solves real fashion problems: when a customer wants Size L-XL specifically, you need to know if that variant is in stock, not just whether the base product exists. Categories and sizes use Enums—can't create invalid entries. Field validators catch edge cases like empty strings.

More complex than a single table, but it mirrors how actual retail inventory works.

Architecture

Everything used to live in main.py. Routes, business logic, database queries, all mixed. Worked for five endpoints. Would've been unmaintainable at fifty.

Now: separation of concerns. main.py launches. Services handle logic. Routes define endpoints. Models define data. When I need to change product creation, I know exactly where to look.

Error Handling

Old way: wrap things in try-catch when they broke, handle locally, hope nothing unexpected happens.

New way: centralized error handler. One place catches exceptions, formats responses consistently, logs properly. Something goes wrong anywhere, it gets handled the same way.

Development Workflow

Biggest change wasn't the code—it was how I wrote it. Version one was commits to main whenever something worked. Version two uses pull requests for every feature. Every change gets reviewed before merge.

Felt slow. It is slower. But "slower" means "catching issues before production" and "understanding why patterns matter."

What Code Review Actually Teaches You

Having someone review my code exposed gaps I didn't know existed.

I renamed functions but didn't understand what I was renaming. I was splitting routers from controllers mechanically but couldn't explain the pattern. When asked "are you sure you only need .venv in dockerignore?" I realized I was thinking about exclusions wrong—should've been excluding everything and whitelisting what I need.

I named a docker service web generically. Got told: "You're api. When you add a frontend later, that's web." Right. I was thinking "web server." Should've been thinking "what does this service actually do."

These weren't "change this line" comments. They were "here's why this matters" and "think about scaling." That's what tutorials don't teach.

Some PRs got approved quickly. Others took three rounds. The ones that took longer taught me more.

The Gaps I'm Still Filling

Rebuilding didn't make me an expert. Made me aware of what I don't know.

I implement CRUD operations but couldn't clearly explain in an interview what CRUD means and why it matters as a pattern. I use an ORM (SQLModel) but can't give a solid answer on when to use ORM versus raw SQL and what the tradeoffs are. I have a centralized error handler but had to look up why it's "error handler" not "exception handler" and what that distinction means.

These are basic things. Interview questions. The fact that I'm building working software while having these gaps means I need to go back to fundamentals—not to learn how to code, but to learn how to articulate what I'm doing and why.

The Result

Original API: archived, private. Not bad code—just a snapshot of where I was three months ago. Not portfolio material.

New version isn't production-ready either, still flat, need tests and a proper folder-structure architecture. I'm honest about that. But it's built on patterns I can explain. When I add tests next week, the architecture supports it. When I build a chatbot layer next month, I'll have confidence in the foundation.

If You're In The Same Position

If you built something that works but can't explain it, consider rebuilding. Not because the first version was wrong—because the second version will teach you things the first couldn't.

You'll see patterns you missed. You'll understand why approaches matter. You'll find knowledge gaps you didn't know existed. When someone asks you to explain design decisions, you'll have answers deeper than "the tutorial told me to."

First version proves you can make it work. Second version proves you understand why it works.

That difference matters.


Transitioning from retail operations to AI engineering. Building a fashion-focused retail API with FastAPI + PostgreSQL. Next: chatbot layer, then RAG, then multi-agent systems. Follow the journey: github.com/chris-kechagias/retail-inventory-api

Top comments (0)