Files
claude-skills-reference/engineering-team/senior-fullstack/references/architecture_patterns.md
Alireza Rezvani 888ad9b584 fix(skill): rewrite senior-fullstack with real fullstack content (#67) (#127)
- Replace placeholder project_scaffolder.py with real implementation
  supporting Next.js, FastAPI+React, MERN, Django+React templates
- Replace placeholder code_quality_analyzer.py with real implementation
  for security, complexity, dependencies, and test coverage analysis
- Delete redundant fullstack_scaffolder.py (functionality in project_scaffolder)
- Rewrite architecture_patterns.md with real patterns:
  frontend architecture, backend patterns, API design, caching, auth
- Rewrite development_workflows.md with real workflows:
  Docker setup, git workflows, CI/CD, testing, deployment strategies
- Rewrite tech_stack_guide.md with real comparisons:
  framework selection, database choices, auth solutions, deployment
- Rewrite SKILL.md with TOC, trigger phrases, actual tool parameters

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 09:11:07 +01:00

13 KiB

Fullstack Architecture Patterns

Proven architectural patterns for scalable fullstack applications covering frontend, backend, and their integration.


Table of Contents


Frontend Architecture

Component Architecture

Atomic Design Pattern

Organize components in hierarchical levels:

src/components/
├── atoms/           # Button, Input, Icon
├── molecules/       # SearchInput, FormField
├── organisms/       # Header, Footer, Sidebar
├── templates/       # PageLayout, DashboardLayout
└── pages/           # Home, Profile, Settings

When to use: Large applications with design systems and multiple teams.

Container/Presentational Pattern

// Presentational - pure rendering, no state
function UserCard({ name, email, avatar }: UserCardProps) {
  return (
    <div className="card">
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  );
}

// Container - handles data fetching and state
function UserCardContainer({ userId }: { userId: string }) {
  const { data, loading } = useUser(userId);
  if (loading) return <Skeleton />;
  return <UserCard {...data} />;
}

When to use: When you need clear separation between UI and logic.

State Management Patterns

Server State vs Client State

Type Examples Tools
Server State User data, API responses React Query, SWR
Client State UI toggles, form inputs Zustand, Jotai
URL State Filters, pagination Next.js router

React Query for Server State:

function useUsers(filters: Filters) {
  return useQuery({
    queryKey: ["users", filters],
    queryFn: () => api.getUsers(filters),
    staleTime: 5 * 60 * 1000, // 5 minutes
    gcTime: 30 * 60 * 1000,   // 30 minutes
  });
}

// Mutations with optimistic updates
function useUpdateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: api.updateUser,
    onMutate: async (newUser) => {
      await queryClient.cancelQueries({ queryKey: ["users"] });
      const previous = queryClient.getQueryData(["users"]);
      queryClient.setQueryData(["users"], (old) =>
        old.map(u => u.id === newUser.id ? newUser : u)
      );
      return { previous };
    },
    onError: (err, newUser, context) => {
      queryClient.setQueryData(["users"], context.previous);
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
  });
}

Backend Architecture

Clean Architecture

src/
├── domain/              # Business entities, no dependencies
│   ├── entities/        # User, Order, Product
│   └── interfaces/      # Repository interfaces
├── application/         # Use cases, application logic
│   ├── use-cases/       # CreateOrder, UpdateUser
│   └── services/        # OrderService, AuthService
├── infrastructure/      # External concerns
│   ├── database/        # Repository implementations
│   ├── http/            # Controllers, middleware
│   └── external/        # Third-party integrations
└── shared/              # Cross-cutting concerns
    ├── errors/
    └── utils/

Dependency Flow: domain ← application ← infrastructure

Repository Pattern:

// Domain interface
interface UserRepository {
  findById(id: string): Promise<User | null>;
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<User>;
  delete(id: string): Promise<void>;
}

// Infrastructure implementation
class PostgresUserRepository implements UserRepository {
  constructor(private db: Database) {}

  async findById(id: string): Promise<User | null> {
    const row = await this.db.query(
      "SELECT * FROM users WHERE id = $1",
      [id]
    );
    return row ? this.toEntity(row) : null;
  }

  private toEntity(row: UserRow): User {
    return new User({
      id: row.id,
      email: row.email,
      name: row.name,
      createdAt: row.created_at,
    });
  }
}

Middleware Pipeline

// Express middleware chain
app.use(cors());
app.use(helmet());
app.use(requestId());
app.use(logger());
app.use(authenticate());
app.use(rateLimit());
app.use("/api", routes);
app.use(errorHandler());

// Custom middleware example
function requestId() {
  return (req: Request, res: Response, next: NextFunction) => {
    req.id = req.headers["x-request-id"] || crypto.randomUUID();
    res.setHeader("x-request-id", req.id);
    next();
  };
}

function errorHandler() {
  return (err: Error, req: Request, res: Response, next: NextFunction) => {
    const status = err instanceof AppError ? err.status : 500;
    const message = status === 500 ? "Internal Server Error" : err.message;

    logger.error({ err, requestId: req.id });
    res.status(status).json({ error: message, requestId: req.id });
  };
}

API Design Patterns

REST Best Practices

Resource Naming:

  • Use nouns, not verbs: /users not /getUsers
  • Use plural: /users not /user
  • Nest for relationships: /users/{id}/orders

HTTP Methods:

Method Purpose Idempotent
GET Retrieve Yes
POST Create No
PUT Replace Yes
PATCH Partial update No
DELETE Remove Yes

Response Envelope:

// Success response
{
  "data": { /* resource */ },
  "meta": {
    "requestId": "abc-123",
    "timestamp": "2024-01-15T10:30:00Z"
  }
}

// Paginated response
{
  "data": [/* items */],
  "pagination": {
    "page": 1,
    "pageSize": 20,
    "total": 150,
    "totalPages": 8
  }
}

// Error response
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input",
    "details": [
      { "field": "email", "message": "Invalid email format" }
    ]
  },
  "meta": { "requestId": "abc-123" }
}

GraphQL Architecture

Schema-First Design:

type Query {
  user(id: ID!): User
  users(filter: UserFilter, page: PageInput): UserConnection!
}

type Mutation {
  createUser(input: CreateUserInput!): UserPayload!
  updateUser(id: ID!, input: UpdateUserInput!): UserPayload!
}

type User {
  id: ID!
  email: String!
  profile: Profile
  orders(first: Int, after: String): OrderConnection!
}

type UserPayload {
  user: User
  errors: [Error!]
}

Resolver Pattern:

const resolvers = {
  Query: {
    user: async (_, { id }, { dataSources }) => {
      return dataSources.userAPI.findById(id);
    },
  },
  User: {
    // Field resolver for related data
    orders: async (user, { first, after }, { dataSources }) => {
      return dataSources.orderAPI.findByUserId(user.id, { first, after });
    },
  },
};

DataLoader for N+1 Prevention:

const userLoader = new DataLoader(async (userIds: string[]) => {
  const users = await db.query(
    "SELECT * FROM users WHERE id = ANY($1)",
    [userIds]
  );
  // Return in same order as input
  return userIds.map(id => users.find(u => u.id === id));
});

Database Patterns

Connection Pooling

// PostgreSQL with connection pool
const pool = new Pool({
  host: process.env.DB_HOST,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  max: 20,                    // Maximum connections
  idleTimeoutMillis: 30000,   // Close idle connections
  connectionTimeoutMillis: 2000,
});

// Prisma with connection pool
const prisma = new PrismaClient({
  datasources: {
    db: {
      url: `${process.env.DATABASE_URL}?connection_limit=20&pool_timeout=10`,
    },
  },
});

Transaction Patterns

// Unit of Work pattern
async function transferFunds(from: string, to: string, amount: number) {
  return await prisma.$transaction(async (tx) => {
    const sender = await tx.account.update({
      where: { id: from },
      data: { balance: { decrement: amount } },
    });

    if (sender.balance < 0) {
      throw new InsufficientFundsError();
    }

    await tx.account.update({
      where: { id: to },
      data: { balance: { increment: amount } },
    });

    return tx.transaction.create({
      data: { fromId: from, toId: to, amount },
    });
  });
}

Read Replicas

// Route reads to replica
const readDB = new PrismaClient({
  datasources: { db: { url: process.env.READ_DATABASE_URL } },
});

const writeDB = new PrismaClient({
  datasources: { db: { url: process.env.WRITE_DATABASE_URL } },
});

class UserRepository {
  async findById(id: string) {
    return readDB.user.findUnique({ where: { id } });
  }

  async create(data: CreateUserData) {
    return writeDB.user.create({ data });
  }
}

Caching Strategies

Cache Layers

Request → CDN Cache → Application Cache → Database Cache → Database

Cache-Aside Pattern:

async function getUser(id: string): Promise<User> {
  const cacheKey = `user:${id}`;

  // 1. Try cache
  const cached = await redis.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }

  // 2. Fetch from database
  const user = await db.user.findUnique({ where: { id } });
  if (!user) throw new NotFoundError();

  // 3. Store in cache
  await redis.set(cacheKey, JSON.stringify(user), "EX", 3600);

  return user;
}

// Invalidate on update
async function updateUser(id: string, data: UpdateData): Promise<User> {
  const user = await db.user.update({ where: { id }, data });
  await redis.del(`user:${id}`);
  return user;
}

HTTP Cache Headers:

// Immutable assets (hashed filenames)
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");

// API responses
res.setHeader("Cache-Control", "private, max-age=0, must-revalidate");
res.setHeader("ETag", generateETag(data));

// Static pages
res.setHeader("Cache-Control", "public, max-age=3600, stale-while-revalidate=86400");

Authentication Architecture

JWT + Refresh Token Flow

1. User logs in → Server returns access token (15min) + refresh token (7d)
2. Client stores tokens (httpOnly cookie for refresh, memory for access)
3. Access token expires → Client uses refresh token to get new pair
4. Refresh token expires → User must log in again

Implementation:

// Token generation
function generateTokens(user: User) {
  const accessToken = jwt.sign(
    { sub: user.id, email: user.email },
    process.env.JWT_SECRET,
    { expiresIn: "15m" }
  );

  const refreshToken = jwt.sign(
    { sub: user.id, tokenVersion: user.tokenVersion },
    process.env.REFRESH_SECRET,
    { expiresIn: "7d" }
  );

  return { accessToken, refreshToken };
}

// Refresh endpoint
app.post("/auth/refresh", async (req, res) => {
  const refreshToken = req.cookies.refreshToken;

  try {
    const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
    const user = await db.user.findUnique({ where: { id: payload.sub } });

    // Check token version (invalidation mechanism)
    if (user.tokenVersion !== payload.tokenVersion) {
      throw new Error("Token revoked");
    }

    const tokens = generateTokens(user);
    setRefreshCookie(res, tokens.refreshToken);
    res.json({ accessToken: tokens.accessToken });
  } catch {
    res.status(401).json({ error: "Invalid refresh token" });
  }
});

Session-Based Auth

// Redis session store
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === "production",
    httpOnly: true,
    sameSite: "lax",
    maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
  },
}));

// Login
app.post("/auth/login", async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);
  req.session.userId = user.id;
  res.json({ user });
});

// Middleware
function requireAuth(req, res, next) {
  if (!req.session.userId) {
    return res.status(401).json({ error: "Authentication required" });
  }
  next();
}

Decision Matrix

Pattern Complexity Scalability When to Use
Monolith Low Medium MVPs, small teams
Modular Monolith Medium High Growing teams
Microservices High Very High Large orgs, diverse tech
REST Low High CRUD APIs, public APIs
GraphQL Medium High Complex data needs, mobile apps
JWT Auth Low High Stateless APIs, microservices
Session Auth Low Medium Traditional web apps