
Originally published on cesalberca.com
Adapted for Codemotion Magazine by César Alberca, Frontend Architect and Codemotion Committee Member
Rethinking Frontend Architecture in the Age of AI
We’re entering an era where frontend systems aren’t just used by humans—they’re also parsed, generated, and interpreted by AI tools.
Modern frameworks like shadcn/ui, design-to-code tools like V0.dev, and IDE agents like Junie are redefining how we build. But with this acceleration comes a greater need for structure.
This guide outlines a practical architecture for:
- Enhancing UI creation with AI tools
- Designing developer-AI collaboration protocols
- Maintaining testability and scalability
1. Define AI Guidelines for Your Codebase
Most AI tools lack intuition—so give them one. Create a guidelines.md file to describe the rules, stack, and patterns used in your frontend.
Example:
Framework: Next.js
Styling: Tailwind CSS 4
Language: TypeScript
Tests: Vitest + Playwright
Architecture:
- Use Case pattern
- Middleware chaining
Standards:
- Named exports only
- Use union types, no enums
- FC with PropsWithChildren
Code language: HTTP (http)
IDE agents like Junie will use this to follow your conventions without repeated prompts. See a real world example here.
2. Embrace Component-Driven UI with AI Assistance
With tools like V0.dev, you can go from prompt to JSX in seconds:
“Create a dark mode signup form with two input fields and a CTA button”
V0 responds with accessible, styled JSX using shadcn/ui, ready to drop into your repo. Combined with Storybook, MDX docs, and Junie guidelines, you get a feedback loop between AI and your design system.
AI generates. Your system validates.
3. Use Case Pattern for Business Logic
In AI-augmented development, generating UI is only part of the equation. The real value lies in decoupling business logic from interface layers, so AI agents (or humans) can trigger behaviors safely and predictably. That’s where the Use Case pattern to me becomes essential.
What Is the Use Case Pattern?
At its core, each use case encapsulates a single business operation. For example: RegisterUser, CreatePost, SubmitFeedback, etc. It defines a contract like this:
export interface UseCase<In = unknown, Out = unknown> {
handle(param?: In, meta?: UseCaseOptions): Promise<Out>
}
Code language: HTML, XML (xml)
Each use case is an isolated, testable, and injectable unit that operates on input and returns output. It is completely decoupled from UI frameworks or event sources—meaning it can be triggered by:
- A UI button
- A background job
- An AI agent
- A webhook
- Or a combination of all
Why Is This Important for AI?
By providing a stable interface and hiding the internal logic, the Use Case pattern allows AI to orchestrate application behavior without knowing the implementation details.
It prevents AI tools (or humans) from tightly coupling logic with components or views, which often leads to fragile, unmaintainable code.
Centralized Execution with useCaseService
To execute use cases, we use a service layer that abstracts the orchestration logic and allows middleware composition (we’ll see middlewares on a later section):
await useCaseService.execute(RegisterUserUseCase, {
email: "test@example.com",
password: "secure-password",
})
Code language: CSS (css)
This call doesn’t expose the logic inside RegisterUserUseCase—AI just knows it needs to pass a payload and receive a result.
Benefits in AI-Augmented Systems
- Testability: Each use case can be tested in isolation with mocks or stubs.
- Security: Access control or permission checks can be added via middleware.
- Extensibility: New behaviors (e.g. analytics, error reporting) can be layered without touching use case logic.
- AI-Readiness: Agents can safely interact with your backend through a stable, well-defined API surface.
4. Middleware Chains: Scaling Logic with Composability
In a well-structured frontend architecture, cross-cutting concerns—like logging, error handling, and caching—can easily pollute business logic if not managed carefully. Middleware offers a clean, modular approach inspired by the Chain of Responsibility pattern.
Rather than embedding this logic in every use case, you wrap it using middleware classes that intercept execution before and after the core logic runs:
interface Middleware {
intercept<In, Out>(
param: In,
next: UseCase<In, Out>,
options: UseCaseOptions
): Promise<Out>
}
Code language: HTML, XML (xml)
Each middleware is responsible for a single concern and passes control along the chain.
Error Middleware
Instead of writing repetitive try/catch blocks, you can delegate error management to a middleware. When an exception occurs, it dispatches an error event that your UI can listen to—showing a toast or logging the issue—without touching your business logic.
export class ErrorMiddleware implements Middleware {
constructor(private readonly eventEmitter: EventEmitter) {}
async intercept(params: unknown, next: UseCase, options: UseCaseOptions): Promise<unknown> {
try {
return await next.handle(params)
} catch (error) {
if (!options.silentError) {
this.eventEmitter.dispatch(EventType.ERROR, error)
}
throw error
}
}
}
Code language: JavaScript (javascript)
Log Middleware
This middleware logs each use case execution, including the name and parameters. You can plug in different logger implementations—console in development, remote logging in production—without changing any use case code.
export class LogMiddleware implements Middleware {
constructor(private readonly logger: Logger) {}
intercept(params: unknown, useCase: UseCase): Promise<unknown> {
this.logger.log(
`[${DateTime.fromNow().toISO()}] ${this.getName(useCase)} / ${this.printResult(params)}`
)
return useCase.handle(params)
}
private getName(useCase: UseCase): string {
if (useCase instanceof UseCaseHandler) {
return this.getName(useCase.useCase)
}
return useCase.constructor.name
}
private printResult(result: unknown) {
return JSON.stringify(result, null, 2)
}
}
Code language: JavaScript (javascript)
Cache Middleware
If you’re using CQRS to separate queries and commands, this middleware can automatically cache query results. Use cases that provide a cacheKey option will return cached results for a set time-to-live, reducing unnecessary processing.
type CacheEntry = {
value: unknown
expiresAt: number
}
export class CacheMiddleware implements Middleware {
private readonly store = new Map<string, CacheEntry>()
constructor(private readonly ttlInSeconds: number = 60) {}
async intercept<In, Out>(
params: In,
next: UseCase<In, Out>,
options: UseCaseOptions,
): Promise<Out> {
const key = options.cacheKey
if (!key) return next.handle(params, options)
const now = Date.now()
const cached = this.store.get(key)
if (cached && now < cached.expiresAt) {
return cached.value as Out
}
const result = await next.handle(params, options)
this.store.set(key, { value: result, expiresAt: now + this.ttlInSeconds * 1000 })
return result
}
}
Code language: JavaScript (javascript)
Composing Middleware with UseCaseService
To apply multiple middlewares, you wrap them around your use cases using a service. This service composes the middleware chain from the outermost to the innermost layer.
export class UseCaseService {
constructor(
private middlewares: Middleware[],
private readonly container: Container,
) {}
async execute<In, Out>(
useCase: Type<UseCase<In, Out>>,
param?: In,
options?: UseCaseOptions,
): Promise<Out> {
const requiredOptions = options ?? { silentError: false }
let next = UseCaseHandler.create({
next: this.container.create(useCase),
middleware: this.container.get<EmptyMiddleware>(EmptyMiddleware.name),
options: requiredOptions,
})
for (let i = this.middlewares.length - 1; i >= 0; i--) {
const current = this.middlewares[i]
next = UseCaseHandler.create({
next,
middleware: current,
options: requiredOptions,
})
}
return next.handle(param) as Promise<Out>
}
}
Code language: JavaScript (javascript)
Why This Matters
With middleware in place, your system gains:
- Centralized error handling
- Consistent logging
- Automatic caching
- Cleaner use cases with zero duplication
You scale functionality across your app without adding complexity to the core business logic.
Summary Table: AI-Augmented Architecture
Layer | Tooling / Pattern | AI Role |
---|---|---|
UI Prototyping | V0.dev + shadcn/ui | JSX generation from prompt |
UI Documentation | MDX + Storybook | Learn from examples |
Logic Layer | Use Case pattern | Create and invoke logic following defined patterns |
Glue Layer | Middleware chain | Enhances without coupling |
IDE Collaboration | .junie/guidelines.md | Align AI to your conventions |
Final Thoughts
We’re no longer just building for browsers—we’re building systems that both humans and intelligent agents can understand and extend.
The future of frontend isn’t automated. It’s augmented.
Design your architecture for:
- Interpretability (for humans and machines)
- Structure (through conventions and patterns)
- Scalability (via use cases and middleware)
This is how we code with AI, not despite it.
Want help implementing this architecture in your team? I offer consulting for frontend architecture and AI-powered workflows. Reach out or read more about my approach at cesalberca.com.