Install
openclaw skills install code-mirrorMirror frontend and backend code across the stack. Invoke when user wants to generate backend from frontend code, generate frontend from backend code, sync existing code across stack, or scaffold full-stack features from one side.
openclaw skills install code-mirrorCode Mirror analyzes existing frontend or backend code and generates the corresponding counterpart on the other side. It understands the structural semantics — API contracts, data models, routing, validation, state management — and produces idiomatic, production-ready code that perfectly mirrors the source.
Activate Code Mirror when the user:
First, understand what the user has and what they want.
Source side detection (read the code the user provides):
| Clues | Side |
|---|---|
useState, useEffect, useCallback, axios, fetch, onClick, ref, component, template, v-for, v-if, ngIf, ngFor, queryClient, useQuery, useMutation, .tsx, .jsx, .vue, .svelte | Frontend |
@app.route, router., req.body, res.json, @Controller, @GetMapping, @PostMapping, @RestController, Model.find, Schema, prisma, entity, repository, BaseModel, Depends, HTTPException, .go, .java, .py with fastapi/django | Backend |
If ambiguous, ask: "Is this frontend or backend code? What framework are you using?"
Target stack detection (infer from existing project or ask the user):
Check the user's project files in the workspace:
| File | Stack |
|---|---|
package.json | Check dependencies for React, Vue, Angular, Express, NestJS |
requirements.txt / pyproject.toml | Python with FastAPI, Django, Flask |
go.mod | Go with Gin, Echo, net/http |
pom.xml / build.gradle | Java with Spring Boot |
Cargo.toml | Rust with Actix, Rocket |
*.csproj | C# with ASP.NET Core |
If no existing project, ask: "What stack do you want me to generate for? (e.g., Express + TypeScript, FastAPI, Go Gin, Spring Boot)"
Read and deeply understand the source code. Extract the following based on side:
1. API Calls — all endpoints the frontend hits
// For each API call, record:
// - HTTP method (GET, POST, PUT, PATCH, DELETE)
// - URL path (/api/users, /api/users/:id)
// - Request body shape (from the data being sent)
// - Response shape (from how the response is used)
// - Query parameters (from URL params)
// - Path parameters (from template strings like `/users/${id}`)
axios.get('/api/users') // GET /api/users → User[]
axios.post('/api/users', { name, email }) // POST /api/users → User
axios.put(`/api/users/${id}`, { name }) // PUT /api/users/:id → User
axios.delete(`/api/users/${id}`) // DELETE /api/users/:id
fetch('/api/users?page=1&limit=20') // GET with query params
2. Data Models — TypeScript interfaces, PropTypes, Zod schemas
// Extract: name, all fields, field types, optional/required markers, enums, default values
interface User {
id: string; // required string
name: string; // required string
email: string; // required string
role: 'admin' | 'user'; // enum
age?: number; // optional number
createdAt: Date; // date
}
3. Form Validation Rules — Zod, Yup, or manual validation
// Extract: field name, validation type (required, email, min, max, pattern, custom)
const schema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().min(18).optional(),
password: z.string().min(8),
});
4. State Management Patterns (optional, for complex apps)
// Extract: store structure, state shape, actions that modify state
const useUserStore = create((set) => ({
users: [],
loading: false,
error: null,
fetchUsers: async () => { ... },
addUser: (user) => set((state) => ({ users: [...state.users, user] })),
}));
1. Route Definitions — every endpoint the backend exposes
# For each route, record:
# - Path (including path parameters like /users/{user_id})
# - HTTP method
# - Request body schema (from Pydantic model or DTO)
# - Query parameters (from function arguments)
# - Response model (from response_model or return type)
# - Status codes (especially 201, 204)
@router.get("/users") # GET /users → List[UserOut]
@router.get("/users/{user_id}") # GET /users/:id → UserOut
@router.post("/users") # POST /users → UserOut
@router.put("/users/{user_id}") # PUT /users/:id → UserOut
@router.delete("/users/{user_id}", status_code=204) # DELETE /users/:id → 204
2. Data Models / Schemas — database models and API schemas
# Extract: name, fields, types, required/optional, validators, relationships
class User(BaseModel):
id: str
name: str = Field(..., min_length=1, max_length=100)
email: EmailStr
role: Literal["admin", "user"] = "user"
age: Optional[int] = Field(None, ge=0, le=120)
created_at: datetime
3. Business Logic (for more complete generation)
# Extract: operations, side effects (email sending, logging), error handling patterns
def create_user(db: Session, user: UserCreate):
# hash password
# check if email exists
# send welcome email
# return created user
pass
4. Authentication/Authorization
# Extract: auth method (JWT, session, API key), protected routes, role requirements
@router.get("/admin/users", dependencies=[Depends(require_admin)])
async def get_admin_users(...):
pass
Generate code following the target stack's best practices. Always produce code that fits naturally into the existing project structure.
After generation, verify consistency and present the code clearly.
Consistency Checklist:
// TODO: implementOutput Format:
For each generated file, show:
Example:
I'll generate the Express backend for your React todo form.
`src/models/Todo.ts`
```typescript
// full file content
src/validators/todoValidator.ts
// full file content
src/routes/todos.ts
// full file content
POST /api/todos expects { title, description } and returns a todo with an id. All validation rules from your frontend form are mirrored in the backend Zod schema.Model (Mongoose):
import mongoose, { Schema, Document } from 'mongoose';
export interface I{{Resource}} extends Document {
{{#each fields}}
{{name}}: {{mongooseType}};
{{/each}}
createdAt: Date;
updatedAt: Date;
}
const {{resource}}Schema = new Schema<I{{Resource}}>(
{
{{#each fields}}
{{name}}: {
type: {{mongooseType}},
required: {{required}}{{#if unique}}, unique: true{{/if}}
},
{{/each}}
},
{ timestamps: true }
);
export const {{Resource}}Model = mongoose.model<I{{Resource}}>('{{Resource}}', {{resource}}Schema);
Validator (Zod):
import { z } from 'zod';
export const create{{Resource}}Schema = z.object({
{{#each fields}}
{{name}}: z.{{zodType}}(){{#if required}}{{else}}.optional(){{/if}}{{#if validation}}.{{validation}}{{/if}},
{{/each}}
});
export const update{{Resource}}Schema = create{{Resource}}Schema.partial();
export type Create{{Resource}}Input = z.infer<typeof create{{Resource}}Schema>;
export type Update{{Resource}}Input = z.infer<typeof update{{Resource}}Schema>;
Route:
import { Router } from 'express';
import { {{Resource}}Model } from '../models/{{Resource}}';
import { validate } from '../middleware/validate';
import { create{{Resource}}Schema, update{{Resource}}Schema } from '../validators/{{resource}}Validator';
import { authMiddleware } from '../middleware/auth';
const router = Router();
// Apply auth middleware to all routes (if needed)
router.use(authMiddleware);
router.get('/', async (req, res) => {
const items = await {{Resource}}Model.find();
res.json(items);
});
router.get('/:id', async (req, res) => {
const item = await {{Resource}}Model.findById(req.params.id);
if (!item) return res.status(404).json({ error: '{{Resource}} not found' });
res.json(item);
});
router.post('/', validate(create{{Resource}}Schema), async (req, res) => {
const item = await {{Resource}}Model.create(req.body);
res.status(201).json(item);
});
router.put('/:id', validate(update{{Resource}}Schema), async (req, res) => {
const item = await {{Resource}}Model.findByIdAndUpdate(req.params.id, req.body, { new: true });
if (!item) return res.status(404).json({ error: '{{Resource}} not found' });
res.json(item);
});
router.delete('/:id', async (req, res) => {
const item = await {{Resource}}Model.findByIdAndDelete(req.params.id);
if (!item) return res.status(404).json({ error: '{{Resource}} not found' });
res.status(204).end();
});
export default router;
Schema (Pydantic):
from pydantic import BaseModel, Field, EmailStr
from typing import Optional
from datetime import datetime
from enum import Enum
class {{Resource}}Role(str, Enum):
{{#each enums}}
{{value}} = "{{value}}"
{{/each}}
class {{Resource}}Base(BaseModel):
{{#each fields}}
{{name}}: {{pythonType}} = Field(
{{#if required}}...{{else}}None{{/if}},
{{#if validation}}...{{/if}}
)
{{/each}}
class {{Resource}}Create({{Resource}}Base):
{{#each requiredFields}}
{{name}}: {{pythonType}}
{{/each}}
class {{Resource}}Update(BaseModel):
{{#each fields}}
{{name}}: Optional[{{pythonType}}] = None
{{/each}}
class {{Resource}}Out({{Resource}}Base):
id: str
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
Router:
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from typing import List
from app.database import get_db
from app.schemas.{{resource}} import {{Resource}}Create, {{Resource}}Update, {{Resource}}Out
from app import crud
router = APIRouter(prefix="/{{resource}}s", tags=["{{resource}}s"])
@router.get("", response_model=List[{{Resource}}Out])
async def list_{{resource}}s(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=100),
db: Session = Depends(get_db),
):
return crud.{{resource}}.get_multi(db, skip=skip, limit=limit)
@router.get("/{{item_id}}", response_model={{Resource}}Out)
async def get_{{resource}}({{item_id}}: str, db: Session = Depends(get_db)):
item = crud.{{resource}}.get(db, id={{item_id}})
if not item:
raise HTTPException(status_code=404, detail="{{Resource}} not found")
return item
@router.post("", response_model={{Resource}}Out, status_code=201)
async def create_{{resource}}(
item: {{Resource}}Create,
db: Session = Depends(get_db),
):
return crud.{{resource}}.create(db, obj_in=item)
@router.put("/{{item_id}}", response_model={{Resource}}Out)
async def update_{{resource}}(
{{item_id}}: str,
item: {{Resource}}Update,
db: Session = Depends(get_db),
):
db_item = crud.{{resource}}.get(db, id={{item_id}})
if not db_item:
raise HTTPException(status_code=404, detail="{{Resource}} not found")
return crud.{{resource}}.update(db, db_obj=db_item, obj_in=item)
@router.delete("/{{item_id}}", status_code=204)
async def delete_{{resource}}({{item_id}}: str, db: Session = Depends(get_db)):
item = crud.{{resource}}.get(db, id={{item_id}})
if not item:
raise HTTPException(status_code=404, detail="{{Resource}} not found")
crud.{{resource}}.remove(db, id={{item_id}})
Types:
// types/{{resource}}.ts
export interface {{Resource}} {
id: string;
{{#each fields}}
{{name}}: {{tsType}};
{{/each}}
createdAt: string;
updatedAt: string;
}
export interface Create{{Resource}}Input {
{{#each requiredFields}}
{{name}}: {{tsType}};
{{/each}}
{{#each optionalFields}}
{{name}}?: {{tsType}};
{{/each}}
}
export interface Update{{Resource}}Input {
{{#each fields}}
{{name}}?: {{tsType}};
{{/each}}
}
API Client:
// api/{{resource}}.ts
import axios from 'axios';
import type { {{Resource}}, Create{{Resource}}Input, Update{{Resource}}Input } from '../types/{{resource}}';
const BASE_URL = '/api/{{resource}}s';
export const {{resource}}Api = {
list: () => axios.get<{{Resource}}[]>(BASE_URL),
getById: (id: string) => axios.get<{{Resource}}>(`${BASE_URL}/${id}`),
create: (data: Create{{Resource}}Input) => axios.post<{{Resource}}>(BASE_URL, data),
update: (id: string, data: Update{{Resource}}Input) =>
axios.put<{{Resource}}>(`${BASE_URL}/${id}`, data),
delete: (id: string) => axios.delete(`${BASE_URL}/${id}`),
};
React Query Hooks:
// hooks/use{{Resource}}s.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { {{resource}}Api } from '../api/{{resource}}';
import type { Create{{Resource}}Input, Update{{Resource}}Input } from '../types/{{resource}}';
export function use{{Resource}}s() {
return useQuery({
queryKey: ['{{resource}}s'],
queryFn: async () => {
const { data } = await {{resource}}Api.list();
return data;
},
});
}
export function use{{Resource}}(id: string) {
return useQuery({
queryKey: ['{{resource}}s', id],
queryFn: async () => {
const { data } = await {{resource}}Api.getById(id);
return data;
},
enabled: !!id,
});
}
export function useCreate{{Resource}}() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (input: Create{{Resource}}Input) => {{resource}}Api.create(input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['{{resource}}s'] });
},
});
}
export function useUpdate{{Resource}}() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, ...input }: Update{{Resource}}Input & { id: string }) =>
{{resource}}Api.update(id, input),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: ['{{resource}}s'] });
queryClient.invalidateQueries({ queryKey: ['{{resource}}s', id] });
},
});
}
export function useDelete{{Resource}}() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => {{resource}}Api.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['{{resource}}s'] });
},
});
}
Composable:
// composables/use{{Resource}}s.ts
import { ref, readonly } from 'vue';
import axios from 'axios';
import type { {{Resource}}, Create{{Resource}}Input, Update{{Resource}}Input } from '../types/{{resource}}';
export function use{{Resource}}s() {
const items = ref<{{Resource}}[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
const currentItem = ref<{{Resource}} | null>(null);
const fetchAll = async () => {
loading.value = true;
error.value = null;
try {
const { data } = await axios.get<{{Resource}}[]>('/api/{{resource}}s');
items.value = data;
} catch (e) {
error.value = (e as Error).message;
} finally {
loading.value = false;
}
};
const fetchOne = async (id: string) => {
loading.value = true;
error.value = null;
try {
const { data } = await axios.get<{{Resource}}>(`/api/{{resource}}s/${id}`);
currentItem.value = data;
return data;
} catch (e) {
error.value = (e as Error).message;
throw e;
} finally {
loading.value = false;
}
};
const create = async (input: Create{{Resource}}Input) => {
const { data } = await axios.post<{{Resource}}>('/api/{{resource}}s', input);
items.value.push(data);
return data;
};
const update = async (id: string, input: Update{{Resource}}Input) => {
const { data } = await axios.put<{{Resource}}>(`/api/{{resource}}s/${id}`, input);
const index = items.value.findIndex(i => i.id === id);
if (index !== -1) items.value[index] = data;
if (currentItem.value?.id === id) currentItem.value = data;
return data;
};
const remove = async (id: string) => {
await axios.delete(`/api/{{resource}}s/${id}`);
items.value = items.value.filter(i => i.id !== id);
if (currentItem.value?.id === id) currentItem.value = null;
};
return {
items: readonly(items),
currentItem: readonly(currentItem),
loading: readonly(loading),
error: readonly(error),
fetchAll,
fetchOne,
create,
update,
remove,
};
}
User says: "My FastAPI backend added a phone field to User. Update my React frontend."
Process:
phone fieldphone: string to User interfacephone to CreateUserInput/UpdateUserInputOutput format for sync:
I found the new phone field in your backend User model. Here are the frontend updates needed:
src/types/user.ts — add phone field:
export interface User {
id: string;
name: string;
email: string;
+ phone: string;
role: 'admin' | 'user';
createdAt: string;
}
src/api/users.ts — no change needed (API calls remain the same)
src/components/UserForm.tsx — add phone input:
const schema = z.object({
name: z.string().min(1),
email: z.string().email(),
+ phone: z.string().regex(/^\+?[1-9]\d{1,14}$/),
});
User says: "Here's my React form for creating a product. Generate the Express backend."
Process:
POST /api/productsname, price, descriptionname required, price positive numberUser says: "I need a todo app with users. Generate React frontend and Express backend."
Process:
id, name, email, password (hashed)id, title, completed, userId, createdAtWhen converting between languages, use these mappings:
| TypeScript | Python (Pydantic) |
|---|---|
string | str |
number | int | float |
boolean | bool |
Date / string (ISO) | datetime |
Array<T> | T[] | List[T] |
Record<string, T> | Dict[str, T] |
T | null | T | undefined | Optional[T] |
interface | BaseModel |
enum | Enum or Literal |
'a' | 'b' | Literal['a', 'b'] |
Partial<T> | Separate Update model (all optional) |
Omit<T, 'id'> | Separate Create model without id |
| TypeScript | Go |
|---|---|
string | string |
number | int | int64 | float64 |
boolean | bool |
Date | time.Time |
Array<T> | T[] | []T |
Record<string, T> | map[string]T |
T | null | *T (pointer) |
interface | struct with JSON tags |
enum | const with iota or string constants |
| TypeScript | Java |
|---|---|
string | String |
number | int | Long | Double |
boolean | boolean | Boolean |
Date | LocalDateTime | Instant |
Array<T> | T[] | List<T> |
Record<string, T> | Map<String, T> |
T | null | Optional<T> or @Nullable |
interface | DTO class with getters/setters |
enum | enum |
| TypeScript | C# |
|---|---|
string | string |
number | int | long | double | decimal |
boolean | bool |
Date | DateTime | DateTimeOffset |
Array<T> | T[] | List<T> | T[] |
Record<string, T> | Dictionary<string, T> |
T | null | T? (nullable reference types) |
interface | class with properties |
User:
Here's my React component for user management. Generate the Express backend for it.
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
function UserList() {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
axios.get('/api/users').then(res => setUsers(res.data));
}, []);
const addUser = async (name: string, email: string, role: string) => {
const res = await axios.post('/api/users', { name, email, role });
setUsers([...users, res.data]);
};
const deleteUser = async (id: number) => {
await axios.delete(`/api/users/${id}`);
setUsers(users.filter(u => u.id !== id));
};
// ... render
}
Code Mirror response:
I'll generate the Express backend that mirrors your React frontend.
From your frontend, I identified:
src/models/User.ts
import mongoose, { Schema, Document } from 'mongoose';
export interface IUser extends Document {
name: string;
email: string;
role: 'admin' | 'user';
createdAt: Date;
updatedAt: Date;
}
const UserSchema = new Schema<IUser>(
{
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
role: { type: String, enum: ['admin', 'user'], default: 'user' },
},
{ timestamps: true }
);
export const UserModel = mongoose.model<IUser>('User', UserSchema);
src/validators/userValidator.ts
import { z } from 'zod';
export const createUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
role: z.enum(['admin', 'user']).default('user'),
});
export const updateUserSchema = createUserSchema.partial();
src/routes/users.ts
import { Router } from 'express';
import { UserModel } from '../models/User';
import { validate } from '../middleware/validate';
import { createUserSchema, updateUserSchema } from '../validators/userValidator';
const router = Router();
router.get('/', async (req, res) => {
const users = await UserModel.find();
res.json(users);
});
router.post('/', validate(createUserSchema), async (req, res) => {
const user = await UserModel.create(req.body);
res.status(201).json(user);
});
router.delete('/:id', async (req, res) => {
const user = await UserModel.findByIdAndDelete(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.status(204).end();
});
export default router;
The routes exactly match your frontend calls. The POST validator enforces the same required fields your frontend sends.
### Example 2: Backend → Frontend (Sync Mode)
**User:**
My FastAPI backend User model added a phone field. Update my React frontend types and form.
Backend model:
class User(BaseModel):
id: str
name: str
email: str
phone: str = Field(..., pattern=r'^\+?[1-9]\d{1,14}$')
role: Literal['admin', 'user'] = 'user'
created_at: datetime
**Code Mirror response:**
I found the new phone field. Here are the frontend updates:
src/types/user.ts — add phone field:
export interface User {
id: string;
name: string;
email: string;
+ phone: string;
role: 'admin' | 'user';
createdAt: string;
}
export interface CreateUserInput {
name: string;
email: string;
+ phone: string;
role?: 'admin' | 'user';
}
src/components/UserForm.tsx — add phone field with validation:
const schema = z.object({
name: z.string().min(1),
email: z.string().email(),
+ phone: z.string().regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number'),
role: z.enum(['admin', 'user']).optional(),
});
src/api/users.ts — no change needed (API calls remain the same, phone will be included automatically in POST/PUT)
Field(pattern=...) constraint.Before finalizing generated code, verify:
// TODO: implement thisAlways include proper error handling appropriate to the stack:
// Express - 404
if (!item) return res.status(404).json({ error: 'Resource not found' });
// Express - validation
if (!result.success) return res.status(400).json({ errors: result.error.flatten() });
// Express - duplicate
try {
await Model.create(data);
} catch (err) {
if (err.code === 11000) return res.status(409).json({ error: 'Duplicate key' });
throw err;
}
# FastAPI - 404
if not item:
raise HTTPException(status_code=404, detail="Resource not found")
# FastAPI - validation (handled automatically by Pydantic)
# FastAPI - duplicate
if existing:
raise HTTPException(status_code=409, detail="Email already registered")
// React Query - errors are automatically available in query result
const { error } = useQuery(...);
// Axios interceptor for global error handling
axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// redirect to login
}
return Promise.reject(error);
}
);
When using this skill: