Complex Project Architecture with AI

Full-Stack Feature Development

5 min read

The Vertical Slice Approach

Instead of building layer by layer, the vertical slice approach builds a complete feature from database to UI. AI assistants excel at this because they can maintain context across all layers.

Case Study: Building a Comments Feature

Let's build a complete comments system for a blog platform.

Phase 1: Planning with Claude Code

claude "Plan a comments feature for our Next.js blog.
Requirements:
- Nested comments (2 levels max)
- Real-time updates
- Markdown support
- Author avatars
- Edit/delete own comments

Analyze our existing patterns and create:
1. Database schema
2. API endpoints
3. Component structure
4. State management approach"

Phase 2: Database Layer

Claude Code generates the schema:

// prisma/schema.prisma additions

model Comment {
  id        String   @id @default(cuid())
  content   String   @db.Text
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relations
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  post      Post     @relation(fields: [postId], references: [id])
  postId    String
  parent    Comment? @relation("CommentReplies", fields: [parentId], references: [id])
  parentId  String?
  replies   Comment[] @relation("CommentReplies")

  @@index([postId])
  @@index([authorId])
  @@index([parentId])
}

Execute migration:

claude "Run the Prisma migration for the Comment model
and generate the client"

Phase 3: API Layer

claude "Create the API routes for comments following our
existing patterns in src/app/api/posts/. Include:
- GET /api/posts/[postId]/comments (with pagination)
- POST /api/posts/[postId]/comments
- PATCH /api/comments/[id]
- DELETE /api/comments/[id]

Use our existing auth middleware and error handling."

Generated route structure:

// src/app/api/posts/[postId]/comments/route.ts

import { NextRequest } from 'next/server';
import { getServerSession } from 'next-auth';
import { prisma } from '@/lib/db';
import { commentSchema } from '@/lib/validations/comment';

export async function GET(
  request: NextRequest,
  { params }: { params: { postId: string } }
) {
  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get('page') ?? '1');
  const limit = parseInt(searchParams.get('limit') ?? '20');

  const comments = await prisma.comment.findMany({
    where: {
      postId: params.postId,
      parentId: null, // Top-level comments only
    },
    include: {
      author: { select: { id: true, name: true, image: true } },
      replies: {
        include: {
          author: { select: { id: true, name: true, image: true } },
        },
        orderBy: { createdAt: 'asc' },
      },
    },
    orderBy: { createdAt: 'desc' },
    skip: (page - 1) * limit,
    take: limit,
  });

  return Response.json({ comments, page, limit });
}

export async function POST(
  request: NextRequest,
  { params }: { params: { postId: string } }
) {
  const session = await getServerSession();
  if (!session?.user) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const body = await request.json();
  const validated = commentSchema.parse(body);

  const comment = await prisma.comment.create({
    data: {
      content: validated.content,
      postId: params.postId,
      authorId: session.user.id,
      parentId: validated.parentId,
    },
    include: {
      author: { select: { id: true, name: true, image: true } },
    },
  });

  return Response.json(comment, { status: 201 });
}

Phase 4: Frontend Components

Switch to Cursor for real-time component development:

In Cursor: "Create a CommentSection component that:
- Fetches comments with React Query
- Renders nested comments with CommentCard
- Has CommentForm for new comments
- Supports optimistic updates
- Uses our existing Avatar and Button components"

Component structure:

// src/components/comments/CommentSection.tsx

'use client';

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { CommentCard } from './CommentCard';
import { CommentForm } from './CommentForm';
import { Skeleton } from '@/components/ui/Skeleton';

interface CommentSectionProps {
  postId: string;
}

export function CommentSection({ postId }: CommentSectionProps) {
  const queryClient = useQueryClient();

  const { data, isLoading } = useQuery({
    queryKey: ['comments', postId],
    queryFn: () => fetchComments(postId),
  });

  const addComment = useMutation({
    mutationFn: (content: string) => postComment(postId, content),
    onMutate: async (content) => {
      // Optimistic update
      await queryClient.cancelQueries(['comments', postId]);
      const previous = queryClient.getQueryData(['comments', postId]);
      queryClient.setQueryData(['comments', postId], (old: any) => ({
        ...old,
        comments: [createOptimisticComment(content), ...old.comments],
      }));
      return { previous };
    },
    onError: (err, content, context) => {
      queryClient.setQueryData(['comments', postId], context?.previous);
    },
    onSettled: () => {
      queryClient.invalidateQueries(['comments', postId]);
    },
  });

  if (isLoading) return <CommentsSkeleton />;

  return (
    <section className="space-y-6">
      <h2 className="text-xl font-semibold">
        Comments ({data?.comments.length ?? 0})
      </h2>

      <CommentForm onSubmit={(content) => addComment.mutate(content)} />

      <div className="space-y-4">
        {data?.comments.map((comment) => (
          <CommentCard
            key={comment.id}
            comment={comment}
            postId={postId}
          />
        ))}
      </div>
    </section>
  );
}

Phase 5: Real-time Updates

claude "Add Supabase real-time subscriptions to CommentSection.
When a new comment is added by another user, it should
appear automatically without refresh."
// Add to CommentSection.tsx

useEffect(() => {
  const channel = supabase
    .channel(`comments:${postId}`)
    .on(
      'postgres_changes',
      {
        event: 'INSERT',
        schema: 'public',
        table: 'Comment',
        filter: `postId=eq.${postId}`,
      },
      (payload) => {
        queryClient.invalidateQueries(['comments', postId]);
      }
    )
    .subscribe();

  return () => {
    supabase.removeChannel(channel);
  };
}, [postId, queryClient]);

Phase 6: Integration Testing

claude "Write integration tests for the comments feature:
1. API route tests with MSW
2. Component tests with Testing Library
3. E2E test with Playwright for the full flow"

Coordination Tips for Full-Stack Features

1. Type Sharing

Generate types from your database schema:

claude "Create TypeScript types for Comment that can be
shared between API and frontend. Place in src/types/."

2. Validation Sharing

// src/lib/validations/comment.ts
import { z } from 'zod';

export const commentSchema = z.object({
  content: z.string().min(1).max(10000),
  parentId: z.string().optional(),
});

// Used in both API route and frontend form
export type CommentInput = z.infer<typeof commentSchema>;

3. Error Handling Consistency

claude "Ensure our comment error handling follows the
pattern established in src/lib/errors.ts. Map database
errors to appropriate HTTP status codes."

Iterative Refinement

After the initial implementation:

claude "Review the comments feature we just built:
1. Check for N+1 query problems
2. Verify proper error boundaries
3. Ensure accessibility compliance
4. Identify missing edge cases"

AI identifies improvements:

## Review Findings

1. **N+1 Query**: Fixed - using includes correctly
2. **Error Boundary**: Missing - add ErrorBoundary around CommentSection
3. **Accessibility**: Add aria-labels to action buttons
4. **Edge Cases**:
   - Handle deleted parent comment (orphan replies)
   - Handle deleted author (show "[deleted]")
   - Rate limiting on POST endpoint

Next Lesson

We'll explore monorepo-specific patterns and how to coordinate AI across multiple packages. :::

Quiz

Module 2: Complex Project Architecture with AI

Take Quiz