- 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>
548 lines
13 KiB
Markdown
548 lines
13 KiB
Markdown
# Fullstack Architecture Patterns
|
|
|
|
Proven architectural patterns for scalable fullstack applications covering frontend, backend, and their integration.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
- [Frontend Architecture](#frontend-architecture)
|
|
- [Backend Architecture](#backend-architecture)
|
|
- [API Design Patterns](#api-design-patterns)
|
|
- [Database Patterns](#database-patterns)
|
|
- [Caching Strategies](#caching-strategies)
|
|
- [Authentication Architecture](#authentication-architecture)
|
|
|
|
---
|
|
|
|
## 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**
|
|
|
|
```typescript
|
|
// 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:**
|
|
|
|
```typescript
|
|
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:**
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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:**
|
|
|
|
```typescript
|
|
// 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:**
|
|
|
|
```graphql
|
|
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:**
|
|
|
|
```typescript
|
|
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:**
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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:**
|
|
|
|
```typescript
|
|
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:**
|
|
|
|
```typescript
|
|
// 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:**
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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 |
|