claude-skills/engineering-team/skills/senior-fullstack/references/architecture_patterns.md
Reza Rezvani 1851c8fb09 fix(plugins): restructure 9 multi-skill domain plugins into ./skills/ layout
Same root cause as #587/#591 — Claude Code's runtime loader rejects
array-form skills paths like ["./content-production", "./ai-seo", ...]
even when each entry is a valid subdirectory containing SKILL.md.
`claude plugin validate` accepts them but the loader does not.

The proven canonical layout (used by self-improving-agent in #536):

  <plugin>/
  ├── .claude-plugin/plugin.json    skills: "./skills"
  └── skills/
      ├── <skill-1>/SKILL.md
      ├── <skill-2>/SKILL.md
      └── ...

Restructured 9 multi-skill domain plugins:
- business-growth (4 skills moved)
- c-level-advisor (28)
- engineering (36)
- engineering-team (32)
- finance (2)
- marketing-skill (43)
- product-team (12)
- project-management (8)
- ra-qm-team (13)

Also fixed standalone plugins that had root SKILL.md + ./skills/ subdir
(agenthub, autoresearch-agent, executive-mentor, playwright-pro). The
loader rejected them despite skills="./skills" because of the conflicting
root SKILL.md (compare self-improving-agent which works because PR #536
moved its root SKILL.md). Moved each root SKILL.md into ./skills/<name>/.

Restored standalone plugin folders to their original paths after the
multi-skill restructure swept them into parent skills/ directories
(marketplace.json source paths require original locations).

Removed 7 orphaned marketplace entries that pointed to skill folders
without their own plugin.json (content-creator, demand-gen,
fullstack-engineer, aws-architect, product-manager, scrum-master,
skill-security-auditor) — these were already non-functional.

Bumped patch versions on every changed plugin and synced
marketplace.json. Marketplace now lists 29 working plugins (down
from 36).

After merge: users run `/plugin marketplace update claude-code-skills`
followed by `/plugin update --all` to pick up the working layout.
2026-05-02 22:51:20 +02: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