Skip to content
Back to Blog
Agentic AI7 min read

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:

  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 this article
N

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.