← Back

Trying Out Claude Code - Part 1

Building an MVP from scratch with Claude Code.

16 min read

At this point, the hype around LLMs in software development has been pretty much impossible to ignore. In particular, with the rapid fire release of Opus 4.5, Gemini 3, and ChatGPT 5.2 Codex, it seems many in the programming space have started to change their minds about the viability of “agentic coding”. This means using LLMs and special harnesses like Claude Code or Cursor to write a large portion of a codebase. These are not all hacks or shameless promoters either - Antirez (the creator of Redis), Addy Osmani (former lead at Google Chrome), and DHH (semi-controversial creator of Ruby on Rails), have all chimed in with how effective these different tools are for writing code, even in larger codebases or non trivial tasks.

At this point, I would wager the vast majority of software developers use AI in some way in their own workflows.

I’m of course not exempt. I started using Cursor early in 2025 for coding, but my use of AI was minimal to start out. Cursor has a handy chat sidebar, where you can interact with LLMs in order to ask questions about your codebase, debug code, and of course, prompt new feature changes.

I was (and still am) a big fan of Cursor, and use it daily at my job. However, I normally keep it on a leash. Working on a mid-sized codebase with around ~8 other developers means cranking out code as fast as possible is not a great idea if the quality and safety of the code can’t be vouched for. Sometimes I use Cursor to ask questions, brainstorm, rubber-ducky, and write code, but it’s always be under scrutiny and supervision. There is no way I was going to let unvetted AI generated code enter my codebase. It saves me time, but Cursor is also fantastic for learning new APIs and asking questions about features I did not write, and that provided as much value to me as the actual agentic coding.

Cursor was my mainstay, and although I had heard of other tools (namely Claude Code), I didn’t see a reason to check them out.

But with the release of these more powerful models, the consensus seems to be that the models themselves are important, but an effective “harness” is equally as required for great results with agentic coding. And many people had said they preferred Claude Code over Cursor, and that it had better intuition when it came to understanding the codebase and writing code.

So amid all of these rumblings online, I decided to put it to the test. If I created a greenfield project, could I make a bug-free, secure web app without writing any code myself?

I downloaded Claude Code, and decided to put it to the test on a simple app idea I had been contemplating for a couple of months.

The app

The app is simple CRUD: a customizable way for people to upload their interests and write commentary on them. Different tabs would be different categories of interests (movies, music, books, etc.) that would each contain links to short blog articles written by the user. Sort of like a mix between Linktree and Letterboxd.

I wanted to make this app for a simple reason… I could use it! I feel like theres so many pieces of media that I’ve enjoyed over the years, but I’ve forgotten a my original analyses of them. I could have an organized way of storing my thoughts, that I could come back to whenever i wanted.

OK with that out of the way, how do I actually get this done? This is just an idea, and I don’t have any designs yet.

Designs using AI

So full disclosure - I’m not a designer. I enjoy doing it occasionally, and I can make it work in a pinch if I have to, but it takes me SO LONG. So I decided to use AI to help me out a bit.

First I came up with a minimal set of Figma documents for what I wanted, helped with AI of course. There were a few key pages:

Explore page - For the user to find other users on the app, and view their media.

Explore page

Media List Page - Where a user has their different media categories, with thumbnails of these and links to their reviews. This would include an edit mode, where users can add new categories, tabs, and adjust the theme.

Media List page

Media Review Page - A short blog-style article where a user can write a markdown section for their review. There would be an edit mode and a create mode for this page.

Media Review page

Profile page - where a user can update their profile (bio, profile pic, delete their account, log out, etc.)

Profile page

So as you can see, a fairly simple CRUD app. The only partially annoying parts would be adding the edit mode for the media review and media list pages, the integration of the markdown viewer/editor, and the media upload.

I had a very primitive, straightforward approach for generating these designs. All I did was open up a new chat in claude, provided a short description of my app, and started asking it for feature ideas on each page.

I started with the media list page, where I asked for a mockup in html + css. Once I had that, which was conveniently rendered directly in the console, I could provide follow up questions and design improvements for what I saw.

I repeated this process for the different pages, each time in a new claude chat. At the end, I had a decent set of designs. I was planning on finalizing the UI later and using shadcn + tailwind anyways, so I was more focused on getting the structure of the page. The pages above are what I was left with.

Overall, not bad! Definitely good enough to start.

One regret I did have though, is doing these early designs in the Claude console. It would have been much more efficient to simply make a static react frontend, and have it generate the code there. That way, I could have the code all in one place, be able to enforce consistent styling more easily, and the agent could reference other pages when making design updates to the current one. By having a new chat for each page, I was essentially starting from scratch, and had to copy paste my overall spec document to each chat.

With these designs though, as well as a new overall spec document, I decided to jump into coding!

Starting out

I said before that I had mainly used Cursor for most of my AI-coding needs. Part of the reason why I chose Cursor, and why I was reluctant to try out Claude Code, was because Cursor was very familiar. For those of you who don’t know, it is a fork of VSCode, the go-to text editor for most web developers. I’ve been using VSCode for years, and I’m comfortable with it and the all of the commands, shortcuts, plugins, etc. So when I was first looking into AI coding options about, Cursor seemed like a great choice because it wasn’t big switch up. The same look, feel, and function of VSCode, just with a handy LLM chat window in the side. It even let me import all of my settings and plugins from VSCode.

Claude Code is a different workflow. It’s just a CLI where developers can post in commands and have the agent execute them. At first I was turned off by this. I assumed it meant I wouldn’t be able to easily navigate the code changes made by the agent and decide what I wanted to keep and what to change.

Fortunately, after I downloaded Claude Code and started making changes, I saw this wouldn’t be a huge issue. I could use Claude Code to make changes for a feature, and review the diffs in the git tab of Cursor.

Downloading Claude was a breeze as well. All you have to do is run a command in your terminal, navigate to your codebase, and run Claude. And then you have the terminal pop up. I could actually run it right there in my Cursor editor.

Scaffolding the Project

I wanted to minimize friction and finish an MVP ASAP for this project, so I decided to use a stack I was familiar with:

  • Frontend: shadCN, React Router 7 Framework Mode, Tailwind, React Query
  • Backend: NestJS + Prisma
  • Auth + File Storage + DB: Supabase

I had actually not used Supabase before, but it’s a very convenient option for small apps, with a generous free tier, and I simply did not feel like setting up the Auth, File Storage, and DB using three different services, or from scratch.

To set up the scaffolding, I simply asked Claude to initialize a new project, listing my preferred tech stack for the frontend and backend. It worked very well for initializing the repos from scratch.

With the boilerplate out of the way, I’ll get into the actual coding experience.

The coding

Overall: Claude Code was impressive when it came to writing code. As I had heard, it performed noticeably (but not drastically) better than Cursor in agentic coding. That said, I still had to make a fair number of interventions and examinations of the output to fix bugs and make sure the app was stable. In the sections below, I’ll go over my thoughts in depth and some of the problems and corrections needed for each feature. If you aren’t as interested in the minutae, I recommend skipping to the last section: “Final Thoughts”

Where Claude Code Had Issues

1. Forgetting Established Patterns

A common issue was Claude Code forgetting patterns I had already set up in the codebase. Despite having React Query configured, it would sometimes make raw API calls. Here’s an example function from the frontend, for creating a new category:

const handleCreateCategory = async () => {
  if (!session || !newCategoryName.trim() || !currentTab) return;

  setIsCreatingCategory(true);
  setCategoryError("");

  try {
    await api.tabs.tabsControllerCreateCategory(
      currentTab.id,
      { name: newCategoryName.trim() },
      { headers: { Authorization: `Bearer ${session.access_token}` } }
    );

    setShowAddCategoryModal(false);
    setNewCategoryName("");
    // Reload the page to show the new category in the dropdown
    navigate(0);
  } catch (error: unknown) {
    const err = error as { response?: { data?: { message?: string } } };
    setCategoryError(err.response?.data?.message || "Failed to create category");
  } finally {
    setIsCreatingCategory(false);
  }
};

The problem: it’s using the API directly instead of React Query mutations. It’s also reloading the entire page (navigate(0)) instead of invalidating queries. I had to go back and fix this in multiple places. The same happened with React Hook Form and Shadcn components - Claude would occasionally reach for vanilla implementations instead of the libraries already in use.

Part of this is on me - I should have updated the claude.md with more explicit instructions on using established technology in our codebase, instead of doing things from scratch. I think this will be fixed as the codebase grows though, and there would be a bunch of examples of React Query already in use, so CC would know this pattern.

2. Poor Abstraction Choices

Claude generated this API setup directly in a route file:

const api = new Api({
  baseURL:
    typeof window === "undefined"
      ? process.env.VITE_API_URL || "http://localhost:3000"
      : import.meta.env.VITE_API_URL || "http://localhost:3000",
});

This should obviously be abstracted into a separate file. You shouldn’t be creating a new API instance in every route that needs it. But same with the use of React Query, this seemed to get fixed once there were more examples in the codebase and I explicitly instructed Claude to use this.

3. Limited Scope Awareness

When I asked Claude to fix an auth-related issue, it correctly solved it for the file I was working in - but didn’t propagate the fix to other files with the same problem. I had to explicitly ask it to search the codebase and update other affected areas, and this took me a bit longer to catch onto because I was not as familiar with Supabase.

The auth issue itself was significant: since the auth was originally JWT with Supabase, the loader is unaware if the user is logged in or not. The client side API requests have the JWT, but the server side logic doesn’t have a cookie. This is a huge problem for features like bookmarks - we need to know if each review is bookmarked on the list page, but we have to do this client-side. Claude initially generated a separate endpoint for this:

@Get('reviews/:reviewId/status')
@ApiOperation({ summary: 'Check if a review is bookmarked' })
async isReviewBookmarked(
  @Req() req: AuthenticatedRequest,
  @Param('reviewId') reviewId: string,
): Promise<StandardResponse<{ bookmarked: boolean }>> {
  const bookmarked = await this.bookmarksService.isReviewBookmarked(
    req.user,
    reviewId,
  );
  return StandardResponse.ok({ bookmarked });
}

This doesn’t make sense - the bookmark status should be on the review DTO itself, not a separate API call for each review.

4. SSR Blind Spots

Claude defaulted to client-side patterns that work fine for SPAs but miss out on SSR benefits. Fortunately these were very easy to fix by just prompting Claude in the right direction a few times:

  • Making API calls client-side instead of storing params in URLs
  • Missing debouncing on search inputs

5. Verbose Boilerplate

The generated code was sometimes more verbose than necessary. Here’s an example of an API call in the loader for getting the categories and reviews:

let categories: { id: string; name: string }[] = [];
let reviews: { items: any[]; meta: { page: number; limit: number; total: number; totalPages: number } } = {
  items: [],
  meta: { page: 1, limit: 10, total: 0, totalPages: 0 },
};

if (currentTab) {
  const [categoriesResponse, reviewsResponse] = await Promise.all([
    api.tabs.tabsControllerFindCategoriesForTab(currentTab.id),
    api.tabs.tabsControllerFindReviewsForTab(currentTab.id, { search, categoryId }),
  ]);
  categories = categoriesResponse.data.data || [];
  reviews = reviewsResponse.data.data || reviews;
}

What’s happening here? It’s providing a fallback for reviews with empty data. But it’s far more succinct to just make the reviews the same type as reviewsResponse.data.data (the PaginatedReviewsDto from the generated API types). The reviews service also ended up with extensive interface definitions (ReviewUser, ReviewTab, ReviewCategoryRelation, RelatedReviewRelation) and mapping logic that could have been simplified.

6. Ugly URLs

On the frontend, for a user’s media list page, on a specific tab the URL was originally this:

http://localhost:5173/filmfanatic/f36d90e6-20c9-4610-925a-363cc02787c0?category=0393d857-6fa2-4542-a403-b46a040f7ad3

Clearly this doesn’t look the best! It’s a very long URL and the IDs are a bit confusing. We needed slugs for the category and the tab, but this was a very easy fix to make. Just one of those things you need to keep an eye out for.

7. Debugging DTO Issues

Updating the user bio didn’t work initially. This was because I had validatorPipe: whitelist: true configured. Took some debugging to realize the issue was in my updateUserDto:

@ApiPropertyOptional()
@IsOptional()
@IsString()
bio?: string;

The decorators were correct, but the whitelist setting was stripping the field. It took some back and forth with Claude before I noticed that this was the issue, and there were a few places in the codebase I had to add Class Transformer annotations to make sure the fields weren’t stripped.

Where Claude Code performed well

1. Smart Optimizations

I was genuinely impressed when Claude automatically grouped two API calls into a Promise.all, without me asking:

const [categoriesResponse, reviewsResponse] = await Promise.all([
  api.tabs.tabsControllerFindCategoriesForTab(currentTab.id),
  api.tabs.tabsControllerFindReviewsForTab(currentTab.id, { search, categoryId }),
]);

I had added the categories call first, and when I asked for reviews, Claude recognized both could run concurrently. This kind of optimization is easy to miss when coding manually.

2. Following Patterns Once Established

After I clarified how I wanted the backend structured (DTOs, paginatedResponse decorators, separate modules), Claude consistently followed those patterns. It was notably better than Cursor at remembering to run the API generator after backend changes.

3. Integration Walkthroughs

The Supabase integration went smoothly. Claude walked me through bucket creation and policy attachment step by step. When I hit an error with YouTube ID storage, it helped me debug it (turned out I just needed to restart the backend server).

I haven’t worked with markdown editors in a while. Claude gave me a working solution very quickly. That being said, part of me thinks this is a double edged sword. Sure I got a working solution, but am I really aware of why we did this approach instead of using a different library, such as TipTap? This was a task where I think I could have done a bit more planning on these features (which is recommended by Anthropic, using the planning mode) in order to weigh the pros and cons of different approaches.

Feature Videos

Click through to see each feature in action:

Explore Page - Search and discover other users
Media List Page - Browse a user’s media categories and reviews
Create Review Page - Write and publish a new review
Profile Page - Manage your account settings

Early Thoughts

I was impressed with Claude Code’s ability to generate code. This was a noticeable improvement over a previous experimentation with Cursor + Sonnet 4.5 during the summer. During that project, I could get the app working, but it took a lot more prompting and course correction. I think a lot of these claims of generating 90% of an app’s code with Claude Code are not that unrealistic.

As it stands now though, this MVP I created is still… a bit lacking. There’s a few small features I still want to add, I have to test it more, and the design is currently dreadful. It’s still using the basic ShadCN styles and the UX needs to be polished.

So, I will probably be back in a few days with the app fully finished, and give my deeper thoughts on Claude Code. See you then!