← All posts

How I Actually Use AI as a Full Stack Developer (and What I Throw Away)

Confession: I use Claude pretty much every day at work. And I delete a lot of what it writes.

I've read a bunch of posts about AI and coding lately, and most of them are either “this changed my life” or “we're all getting replaced”. I've been building web apps for about four years, React on the frontend and Python on the backend, long enough to remember doing all of this without AI. My take is way less dramatic. For some things it's great. For others it's confidently wrong, and it took me a while to figure out which is which.

So this is my list, as of right now. What I keep, what I throw away, and a few rules I try (and sometimes fail) to follow.

My Setup

Nothing fancy. I use Claude for pretty much everything: the chat when I have a question or I'm stuck, and Claude Code in the terminal when I want it to actually touch files in the repo.

Before this job I freelanced for about two years, mostly on my own, with no senior dev to ask when something broke. I learned a lot of stuff the slow way back then, and I think that's why I don't fully trust AI output. I treat it like a new colleague who's really fast, really confident, and has never seen our codebase.

Roughly: the chat is for “why is this happening?” and “explain this to me like I'm tired”. Claude Code is for scaffolding components and small API endpoints, a first round of tests, and small refactors.

What I Keep

CSS and Layout

This is probably where it saves me the most time. I describe the layout in plain words (“sidebar that collapses under 768px, cards that wrap, footer stuck to the bottom”) and I get a working starting point in seconds. It's especially nice for stuff I don't write often enough to remember, like grid-template-areas, clamp() for font sizes, or container queries.

It does love extra wrapper divs and random fixed heights, though. So I always shrink the browser down to 320px and check on my actual phone before I believe it.

Python and APIs

On the backend it's great for the boring parts: a simple REST endpoint, a PostgreSQL query, or a quick Python script to clean up a CSV. Stuff I could write myself, but it gets me there in a minute instead of twenty.

The catch: it'll write a query that works fine on 10 rows and crawls on 10,000, or forget that a field can be None. So I read the SQL before I run it and test with real-ish data, not the three rows in my head.

Debugging

I paste the error, the component or a Python traceback, and what I expected to happen. For cryptic errors (hydration mismatches, weird TypeScript generics errors, the classic “Cannot update a component while rendering a different component”) it's usually faster than googling.

One thing I force myself to do: I ask whyit's happening before I ask for a fix. If I can't explain the bug back in one sentence, I don't commit the fix.

Tests

Give it a component or a Python function and it'll write a pretty decent first batch of tests with Testing Library or pytest. It's also good at pointing out edge cases I forgot (empty list, really long text, someone double-clicking submit).

The catch is that some of those tests pass no matter what. So I break the component on purpose and check that at least one test fails. If nothing goes red, the test is useless.

What I Throw Away

  • Props and APIs that don't exist.It'll happily use a prop a library never had, or an API from two major versions ago. For anything library-related, I check the docs for the version we actually have.
  • Accessibility shortcuts. Clickable divs. Inputs with no label. aria-* attributes sprinkled on things where a normal <button> would just work. I delete these on sight.
  • Over-engineering. Ask for a simple toggle and you might get a custom hook, a context provider and a reducer. It needed one useState.
  • useEffect for everything. Syncing state in an effect when you could just calculate the value during render. It shows up a lot, and it causes the kind of bugs that are annoying to track down later.
  • Catch-all excepts. A Python try/exceptthat quietly swallows every error. The code “works”, and the real bug just hides somewhere else. I only catch the errors I actually expect.
  • Huge diffs.If I ask Claude Code for a small change and it touches 12 files, I'm not reviewing all that. I say no and ask for something smaller.
  • Code I don't understand.Even if it works. If I can't explain it in code review, it doesn't go in. “I don't know, the AI wrote it” is not a sentence I ever want to say out loud in a PR.

A Few Rules I Try to Follow

  1. Understand it before I paste it.Sounds obvious. It isn't, at 6pm on a Friday.
  2. Give it real context. The component, the error, what I expected, the library versions. A lot of my bad answers came from lazy prompts.
  3. Ask for the smallest change possible. Small diffs get reviewed properly. Big ones get rubber-stamped.
  4. Check in the browser, not in the chat. DevTools, a real phone, and keyboard-only for anything interactive. For backend changes, I call the endpoint myself.
  5. Struggle a bit first.If it's something I actually want to get good at, I give myself 15 minutes alone before I ask.
  6. Same review as everything else.AI code goes through the normal PR process. No shortcuts because “Claude wrote it”.
  7. Don't paste what you shouldn't. Secrets, client data, anything company policy says no to.

So, Is It Worth It?

For me, yes. It doesn't replace experience, and it won't. What it does is make me faster at the stuff I already understand, and it's a very patient teacher for the stuff I don't.

I don't think the real risk is that AI writes our code. It's that we stop noticing when it's wrong. So use it, but you're still the one who decides what ships.

If you work across the stack too, I'm curious how you use it.