From Prompting to Spec-Driven Development: My Two-Year Path to Frontend AI Engineering
What I learned by shrinking the decision surface of AI agents.
Two years ago I was writing prompts by hand and hoping for good answers. Today I design systems where humans and agents build frontend software together. Nothing about that journey was a straight line, and every stage ended the same way: with a limit I hit, and a lesson that pushed me to the next level.
This is the roadmap I wish I had when I started. If you are a frontend engineer wondering how to become a real AI engineer, I hope it saves you some detours.
The foundation: understand what is under the hood
Before the stages, one thing that sits under all of them: understanding how LLMs actually work. I spent time with the fundamentals, building small models with TensorFlow.js and Python. You do not need to train frontier models. But once you have seen tokens, context windows and probabilities up close, agent behavior stops looking like magic, and you start predicting where it will fail.
Stage 1: Prompt engineering
Like most people, I started by crafting prompts. Better wording, more examples, clearer roles. It worked, until it did not.
The limit I hit: results depended on how I phrased things that day. Nothing was repeatable, and nothing was shareable with my team.
What it taught me: an intuition for how models respond. That intuition is still useful, but it is a skill, not a system.
Early on I kept a text file of “magic prompts”. My favorite one, for generating React components, opened with ten lines of rules: act as a senior React developer, use hooks, use TypeScript, follow best practices. On Monday it produced exactly what I wanted. On Wednesday, with the same prompt and a slightly different component, it returned a class component and invented its own folder structure. The prompt had not changed. The context had. That was the day I realized I was polishing words instead of designing inputs.
Stage 2: Auto-generated context
The next idea was to stop retyping everything. I started storing context automatically and feeding it back to the model, so it would “remember” the project.
The limit I hit: generated context grows noisy fast. Without curation, the model was reading a lot of information that was stale, redundant or simply wrong.
What it taught me: context is the real input. The quality of an answer is bounded by the quality of what the model reads, so context deserves the same care as code.
I wrote a small script that dumped the file tree, recent diffs and chat history into a context file after every session. For a week it felt like magic, because the agent “remembered” our conventions. Then it started importing a utility we had deleted two sprints earlier and following a naming rule we had abandoned. The context file kept growing, with no owner, no review and no way to tell what was still true.
Stage 3: Deterministic prompting
The fix for noisy context was to make it canonical: maintained files, versioned in the repository, that describe how the project works. Instruction files for Copilot agents, CLAUDE.md-style documents, scoped context for specific areas of the codebase.
The limit I hit: behavior became much more predictable, but I still had one agent trying to do everything, in a codebase with no clear boundaries.
What it taught me: if context is the input, then where context lives matters. A rule that sits next to the code it governs is read at the right time.
Stage 4: Architecture as a guardrail
This was the turning point. We migrated a large frontend to Feature-Sliced Design (FSD) and TypeScript, documenting every decision in architecture decision records. We did it for human reasons: locality, clear dependency direction, reviewable public APIs.
The side effect surprised me. The architecture became a cognitive boundary for agents:
- FSD turns “the whole app” into slices with a defined layer, a defined public surface and a defined dependency direction. An agent no longer needs to understand everything. It needs to understand one territory.
- TypeScript narrows the space of valid outputs. A wrong assumption becomes a compiler error, not a production bug.
In my case, a small example says it all: a test fixture widened a literal type to a plain string, TypeScript flagged it immediately, and the fix was to reuse an existing contract. The type system did the review work before any human or agent had to.
To be honest about the limits: not every boundary is mechanically enforced. Some rules, like how barrels are used, still rely on convention and review until lint rules catch up. And I have no benchmark proving agents got faster. What I can say is that the decision surface each agent faces became much smaller.
Get Manuel del Pozo’s stories in your inbox
Join Medium for free to get updates from this writer.
What it taught me: I stopped trying to make agents smarter, and started giving them smaller territories.
Stage 5: Agentic framework engineering
Once the boundaries existed, specialization became natural. Instead of one general agent, I designed agents and skills with a single responsibility each, applying the same principles I use for software:
- an architecture guardian that only reviews structure and cannot edit files
- a TypeScript advisor that only recommends
- a reviewer focused on correctness
- a family of end-to-end testing agents: one writes tests, one classifies failures, one repairs them
The TypeScript migration work used a tree of skills: orchestrators that coordinate a whole package or feature, iterators that walk through components or modules in dependency order, and leaves that do one small job, like migrating one component at a time. I think of them as orchestrators, repeaters and leaves, and the single responsibility principle is visible at every level.
One lesson here was about removing things. We had an orchestrator agent that coordinated the migration, and we retired it once the skills could be discovered and invoked on their own. Orchestration moved into the skill tree. Less central control, clearer responsibilities.
What it taught me: agent design is software design. SOLID applies, and so do the usual smells: god objects, unclear ownership, hidden coupling.
Stage 6: Spec-driven development
The last step so far: making specifications the source of truth. With a spec-driven workflow, a feature moves through a spec, a plan, a task list and an implementation phase, each with its own artifact. Humans review intent. Agents execute against it.
What it taught me: prompts are disposable, specs are durable. When the contract is explicit, agents stop guessing and humans stop re-explaining.
What a Frontend AI engineer actually does
Looking back, the best prompt engineer is not the real Frontend AI engineer. The difference is in what you design:
- Context: what the agent reads, where it lives and who maintains it.
- Boundaries: architecture and types that limit what an agent can touch.
- Roles: agents and skills with one responsibility each.
- Contracts: specs that humans and agents share.
None of these are prompting techniques. They are architecture.
If you are starting today
- Learn the fundamentals so you can predict model behavior.
- Treat context as code: version it, review it, keep it small.
- Invest in architecture and types before adding more agents.
- Give every agent one job and say what it must not do.
- Write the spec before the prompt.
The tools will keep changing. The principles will not.
What stage are you at? I would love to hear where you got stuck.








