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.
flowchart LR
USER(["User message"])
LOOP{"messages.create<br/>agent loop"}
THINK["Extended thinking<br/>optional"]
TOOL{"stop_reason<br/>tool_use?"}
EXEC["Execute tool<br/>append tool_result"]
DONE(["stop_reason<br/>end_turn"])
USER --> LOOP --> THINK --> TOOL
TOOL -->|Yes| EXEC --> LOOP
TOOL -->|No| DONE
style LOOP fill:#4f46e5,stroke:#4338ca,color:#fff
style THINK fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style DONE fill:#059669,stroke:#047857,color:#fff
# 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:
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
# 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="[email protected]"
/>
{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": "[email protected]"},
headers=auth_headers,
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "[email protected]"
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": "[email protected]"},
headers=auth_headers,
)
# Duplicate should fail
response = await client.post(
"/api/v1/teams/test-team-id/invitations",
json={"email": "[email protected]"},
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:
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
# 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.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.