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. :::