Skip to content
Agentic AI
Agentic AI7 min read5 views

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 TD
    START["How to Use Claude Code for Full-Stack Development"] --> A
    A["Why Claude Code Excels at Full-Stack Wo…"]
    A --> B
    B["Setting Up Your Full-Stack CLAUDE.md"]
    B --> C
    C["Implementing a Feature End-to-End"]
    C --> D
    D["Working Across Languages"]
    D --> E
    E["Database Migration Workflow"]
    E --> F
    F["DevOps and Infrastructure"]
    F --> G
    G["Tips for Full-Stack Productivity"]
    G --> H
    H["Conclusion"]
    H --> DONE["Key Takeaways"]
    style START fill:#4f46e5,stroke:#4338ca,color:#fff
    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:

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

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

flowchart LR
    S0["Step 1: Database Layer"]
    S0 --> S1
    S1["Step 2: API Layer"]
    S1 --> S2
    S2["Step 3: Frontend Components"]
    S2 --> S3
    S3["Step 4: Tests"]
    style S0 fill:#4f46e5,stroke:#4338ca,color:#fff
    style S3 fill:#059669,stroke:#047857,color:#fff
  1. Fix a Python backend endpoint that returns malformed JSON
  2. Update the TypeScript frontend type definitions to match
  3. Modify a Dockerfile to include a new system dependency
  4. Update a Kubernetes deployment manifest with new environment variables
  5. 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:

  1. Read the current model to understand the table structure
  2. Update the SQLAlchemy model with the new column
  3. Generate an Alembic migration: alembic revision --autogenerate -m "add_role_to_invitations"
  4. Review the generated migration for correctness
  5. 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

  1. Keep your CLAUDE.md updated — Every time you adopt a new pattern, add it to CLAUDE.md so Claude follows it in future sessions.

  2. 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."

  3. Use /compact between features — Full-stack features generate a lot of context. Compact the conversation before starting the next feature.

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

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

Share
C

Written by

CallSphere Team

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.