How to Use Claude Code for Full-Stack Development
A practical guide to using Claude Code across the full stack — frontend React/Next.js, backend APIs, databases, DevOps, and end-to-end feature implementation.
Why Claude Code Excels at Full-Stack Work
Full-stack development requires context switching between languages, frameworks, and layers. A single feature might touch a React component, a Next.js API route, a database migration, and a Kubernetes deployment manifest. Traditional AI coding tools struggle with this breadth because they optimize for single-file or single-language completion.
Claude Code's agentic architecture makes it uniquely suited for full-stack work. It can read your frontend code to understand the data shape a component expects, then switch to your backend to implement the matching API endpoint, create the database migration, and update the deployment config — all in one conversation.
Setting Up Your Full-Stack CLAUDE.md
The CLAUDE.md file is your most important configuration for full-stack projects. A well-written memory file prevents Claude from generating code that clashes with your existing patterns.
# Project: SaaSApp
## Architecture
- Frontend: Next.js 14 (App Router), TypeScript, Tailwind CSS
- Backend: FastAPI (Python 3.12), SQLAlchemy 2.0
- Database: PostgreSQL 16 with Alembic migrations
- Cache: Redis 7
- Deployment: K8s (k3s) with hostPath volumes
## Frontend Conventions
- Use server components by default, client components only when needed
- All API calls go through lib/api.ts using fetch
- Forms use react-hook-form with zod validation
- State management: React Query for server state, zustand for client state
- Component structure: components/<Feature>/<Component>.tsx
## Backend Conventions
- API routes: app/api/v1/<resource>.py
- Business logic: app/services/<resource>_service.py
- Database models: app/models/<resource>.py
- Pydantic schemas: app/schemas/<resource>.py
- All endpoints require authentication except those marked public
- Use async/await everywhere — no sync database calls
## Database
- Migrations: alembic revision --autogenerate -m "description"
- Always add indexes for foreign keys and frequently queried columns
- Use UUID primary keys
- Soft delete (deleted_at timestamp) for user-facing resources
## Testing
- Frontend: vitest + react-testing-library
- Backend: pytest with async fixtures
- Run frontend tests: cd frontend && npm test
- Run backend tests: cd backend && pytest -x
Implementing a Feature End-to-End
Let's walk through how Claude Code handles a full-stack feature: adding a "team invitations" system.
Step 1: Database Layer
You: Create a team invitations feature. Users should be able to invite others by email.
Invitations expire after 7 days. The invited user sees pending invitations after login.
Claude Code will start by examining your existing database models:
# Claude Code reads your models directory, then creates:
# backend/app/models/invitation.py
from sqlalchemy import Column, String, DateTime, ForeignKey, Enum
from sqlalchemy.dialects.postgresql import UUID
from app.models.base import Base
import uuid
from datetime import datetime, timedelta
import enum
class InvitationStatus(str, enum.Enum):
PENDING = "pending"
ACCEPTED = "accepted"
EXPIRED = "expired"
REVOKED = "revoked"
class Invitation(Base):
__tablename__ = "invitations"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
team_id = Column(UUID(as_uuid=True), ForeignKey("teams.id"), nullable=False, index=True)
inviter_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
email = Column(String(255), nullable=False, index=True)
status = Column(Enum(InvitationStatus), default=InvitationStatus.PENDING, nullable=False)
expires_at = Column(DateTime, default=lambda: datetime.utcnow() + timedelta(days=7))
created_at = Column(DateTime, default=datetime.utcnow)
accepted_at = Column(DateTime, nullable=True)
Step 2: API Layer
Claude Code then creates the service and routes:
# backend/app/services/invitation_service.py
from datetime import datetime
from sqlalchemy import select, and_
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.invitation import Invitation, InvitationStatus
from app.schemas.invitation import CreateInvitationRequest
from fastapi import HTTPException
class InvitationService:
def __init__(self, db: AsyncSession):
self.db = db
async def create_invitation(
self, team_id: str, inviter_id: str, request: CreateInvitationRequest
) -> Invitation:
# Check for existing pending invitation
existing = await self.db.execute(
select(Invitation).where(
and_(
Invitation.team_id == team_id,
Invitation.email == request.email,
Invitation.status == InvitationStatus.PENDING,
Invitation.expires_at > datetime.utcnow(),
)
)
)
if existing.scalar_one_or_none():
raise HTTPException(409, "An active invitation already exists for this email")
invitation = Invitation(
team_id=team_id,
inviter_id=inviter_id,
email=request.email,
)
self.db.add(invitation)
await self.db.commit()
await self.db.refresh(invitation)
return invitation
async def get_pending_for_email(self, email: str) -> list[Invitation]:
result = await self.db.execute(
select(Invitation).where(
and_(
Invitation.email == email,
Invitation.status == InvitationStatus.PENDING,
Invitation.expires_at > datetime.utcnow(),
)
)
)
return list(result.scalars().all())
Step 3: Frontend Components
Claude Code then switches to the frontend:
// frontend/components/Team/InviteMemberDialog.tsx
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { api } from "@/lib/api";
const inviteSchema = z.object({
email: z.string().email("Please enter a valid email address"),
});
type InviteForm = z.infer<typeof inviteSchema>;
export function InviteMemberDialog({ teamId, onClose }: {
teamId: string;
onClose: () => void;
}) {
const queryClient = useQueryClient();
const { register, handleSubmit, formState: { errors } } = useForm<InviteForm>({
resolver: zodResolver(inviteSchema),
});
const mutation = useMutation({
mutationFn: (data: InviteForm) =>
api.post(`/api/v1/teams/${teamId}/invitations`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["invitations", teamId] });
onClose();
},
});
return (
<form onSubmit={handleSubmit((data) => mutation.mutate(data))}>
<div className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email Address
</label>
<input
{...register("email")}
type="email"
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
placeholder="colleague@company.com"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
)}
</div>
<button
type="submit"
disabled={mutation.isPending}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
>
{mutation.isPending ? "Sending..." : "Send Invitation"}
</button>
{mutation.isError && (
<p className="text-sm text-red-600">
{mutation.error instanceof Error ? mutation.error.message : "Failed to send invitation"}
</p>
)}
</div>
</form>
);
}
Step 4: Tests
Claude Code writes tests for both layers:
# backend/tests/test_invitations.py
import pytest
from httpx import AsyncClient
@pytest.mark.asyncio
async def test_create_invitation(client: AsyncClient, auth_headers: dict):
response = await client.post(
"/api/v1/teams/test-team-id/invitations",
json={"email": "newuser@example.com"},
headers=auth_headers,
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "newuser@example.com"
assert data["status"] == "pending"
@pytest.mark.asyncio
async def test_duplicate_invitation_rejected(client: AsyncClient, auth_headers: dict):
# First invitation
await client.post(
"/api/v1/teams/test-team-id/invitations",
json={"email": "duplicate@example.com"},
headers=auth_headers,
)
# Duplicate should fail
response = await client.post(
"/api/v1/teams/test-team-id/invitations",
json={"email": "duplicate@example.com"},
headers=auth_headers,
)
assert response.status_code == 409
Working Across Languages
Claude Code seamlessly context-switches between languages. In a single session, you might:
- Fix a Python backend endpoint that returns malformed JSON
- Update the TypeScript frontend type definitions to match
- Modify a Dockerfile to include a new system dependency
- Update a Kubernetes deployment manifest with new environment variables
- Write a bash script to run database migrations in CI
Claude Code handles all of this because it does not rely on language-specific tooling — it reads files, understands code at a semantic level, and edits with precision regardless of the language.
Database Migration Workflow
Claude Code integrates well with migration tools:
You: Add a "role" column to the invitations table with values "member" and "admin", defaulting to "member".
Claude Code will:
- Read the current model to understand the table structure
- Update the SQLAlchemy model with the new column
- Generate an Alembic migration:
alembic revision --autogenerate -m "add_role_to_invitations" - Review the generated migration for correctness
- Run the migration against your dev database
DevOps and Infrastructure
Claude Code reads and writes infrastructure files just as naturally as application code:
# Claude Code can generate and modify:
# - Dockerfiles
# - docker-compose.yml
# - Kubernetes manifests (Deployments, Services, Ingress)
# - GitHub Actions workflows
# - Terraform configurations
# - Nginx/Caddy configs
Example prompt: "Add a health check endpoint to the backend and update the Kubernetes deployment to use it as a liveness probe."
Claude Code will create the /health endpoint in your FastAPI app, then update the Kubernetes Deployment manifest with the appropriate livenessProbe and readinessProbe configuration.
Tips for Full-Stack Productivity
Keep your CLAUDE.md updated — Every time you adopt a new pattern, add it to CLAUDE.md so Claude follows it in future sessions.
Work feature-by-feature — Ask Claude to implement one complete feature at a time, across all layers, rather than asking for "all the backend endpoints" then "all the frontend components."
Use /compact between features — Full-stack features generate a lot of context. Compact the conversation before starting the next feature.
Let Claude run the tests — After implementing a feature, say "run the tests and fix any failures." Claude Code excels at the fix-test loop.
Review database migrations carefully — Always review auto-generated migrations before running them. Claude Code can help review them too: "Review this migration for potential data loss."
Conclusion
Claude Code's ability to work across the full stack in a single conversation — reading frontend code, implementing backend logic, writing migrations, and updating infrastructure — makes it one of the most effective tools for full-stack developers. The key is a well-structured CLAUDE.md that captures your project's conventions across all layers.
NYC News
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.