Building a Complete Feature with AI: From Ticket to PR

Last week I picked up a ticket that read: “Add a user preferences panel with theme switching, notification settings, and language selection.” Nothing groundbreaking, but the kind of mid-complexity feature that touches multiple layers of the stack. I decided to build the entire thing with AI assistance and document what that workflow actually looks like in practice.
Here’s the honest, step-by-step breakdown of how it went.
Step 1: Planning with AI
Before writing any code, I fed the ticket description to Claude along with some project context. The prompt looked something like this:
I'm building a user preferences panel for a React + TypeScript app.
It needs: theme switching (light/dark/system), notification settings
(email, push, in-app toggles), and language selection (en, es, fr, de).
We use Zustand for state, React Router v7 for routing, and Tailwind v4
for styling. The API is REST with endpoints at /api/v1/users/:id/preferences.
Can you help me plan the component structure, state shape, and API
integration approach?
The AI gave me a component tree, a Zustand store shape, and suggested breaking the work into three phases: state management, UI components, then API integration. That plan saved me roughly 20 minutes of whiteboarding. Not because I couldn’t do it myself, but because the AI’s first draft was close enough that I only needed to adjust a few things.
Step 2: Scaffolding the State
I asked the AI to generate the Zustand store based on the plan we agreed on:
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface Preferences {
theme: 'light' | 'dark' | 'system';
language: 'en' | 'es' | 'fr' | 'de';
notifications: {
email: boolean;
push: boolean;
inApp: boolean;
};
}
interface PreferencesStore {
preferences: Preferences;
isLoading: boolean;
updatePreference: <K extends keyof Preferences>(
key: K,
value: Preferences[K]
) => void;
fetchPreferences: (userId: string) => Promise<void>;
savePreferences: (userId: string) => Promise<void>;
}
export const usePreferencesStore = create<PreferencesStore>()(
persist(
(set, get) => ({
preferences: {
theme: 'system',
language: 'en',
notifications: { email: true, push: true, inApp: true },
},
isLoading: false,
updatePreference: (key, value) =>
set((state) => ({
preferences: { ...state.preferences, [key]: value },
})),
fetchPreferences: async (userId) => {
set({ isLoading: true });
const res = await fetch(`/api/v1/users/${userId}/preferences`);
const data = await res.json();
set({ preferences: data, isLoading: false });
},
savePreferences: async (userId) => {
const { preferences } = get();
await fetch(`/api/v1/users/${userId}/preferences`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(preferences),
});
},
}),
{ name: 'user-preferences' }
)
);
I reviewed it, added error handling the AI missed on the fetch calls, and moved on. Key lesson: always review generated state management code for missing error states. AI tends to write the happy path first.
Step 3: Building Components
For the UI, I gave the AI each section one at a time. “Build me the theme switcher component using this store” worked better than “build me the entire preferences panel.” Smaller, focused prompts produce better code.
The AI-generated components were about 80% production-ready. I adjusted styling, added proper aria labels it forgot, and fixed a controlled/uncontrolled input warning on the language selector.
Step 4: Writing Tests
This is where AI really shines. I pasted each component and asked for Vitest + Testing Library tests. The AI generated tests for default render states, user interactions, and store integration. It even suggested edge cases I hadn’t considered, like what happens when the API returns a language code that isn’t in our supported list.
it('falls back to English for unsupported language codes', async () => {
server.use(
http.get('/api/v1/users/:id/preferences', () =>
HttpResponse.json({ ...defaults, language: 'jp' })
)
);
render(<PreferencesPanel userId="123" />);
await waitFor(() => {
expect(screen.getByRole('combobox')).toHaveValue('en');
});
});
Step 5: The PR
For the pull request, I asked the AI to summarize the changes based on the diff. It produced a structured description with a summary, what changed, how to test it, and screenshots needed. I edited the tone slightly and submitted.
What I Learned
The entire feature took about 3 hours. Without AI, I’d estimate 5-6 hours. But the time savings isn’t the real win. The real win was consistency. Every component followed the same patterns. Tests were thorough. The PR description was complete.
The key takeaways from building features with AI:
- Plan first, generate second. Don’t jump to code generation. Use AI for planning, then feed that plan back when generating code.
- Small prompts beat big ones. One component at a time, not the whole feature.
- Review everything. AI gets you to 80% fast. The last 20% is where your expertise matters: error handling, accessibility, edge cases.
- Use AI for the boring parts. Test boilerplate, PR descriptions, type definitions. Save your energy for architecture decisions.
AI didn’t replace my judgment on this feature. It replaced my typing. And honestly, that’s exactly what I want from a tool.
Written by
Adrian Saycon
A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.
Discussion (0)
Sign in to join the discussion
No comments yet. Be the first to share your thoughts.
Related Articles

Building and Deploying Full-Stack Apps with AI Assistance
A weekend project walkthrough: building a full-stack task manager from architecture planning to deployment, with AI as t

AI-Assisted Database Design and Query Optimization
How to use AI for schema design, index recommendations, N+1 detection, and query optimization in PostgreSQL and MySQL.

Automating Repetitive Tasks with AI Scripts
Practical patterns for using AI to generate automation scripts for data migration, file processing, and scheduled tasks.