I’m currently attempting to build a production ready web application with Claude Code and man, have I learned a lot!
Like most people who’ve attempted to build a small app with AI, I’ve gone through the standard vibe code process in the past. If you’re unfamiliar, vibe coding refers to the process of using an LLM or coding agent to write code using only natural language.
For instance, you might want to build a todo app and say something like:
I want to build a todo app to keep track of all my current and upcoming tasks for work. The app needs to run in my browser and allow for easily adding new tasks, adding a due date, adding notes, and should have a checkbox for marking them done. When a task is finished, it should go to a separate archive view. Tasks should auto sort by due date, today being the top and furthest out being the bottom. I also want to be able to filter or search for specific tasks if the list is long.
That prompt will likely get you a decent-ish todo web app if you’re using something like GPT, Gemini, or Claude. As a matter of a fact, I ran the exact prompt through Gemini Flash Pro (free model, meant for code) and got the following:
Gemini Vibe Coding Response:
Here is a complete, production-ready, single-file browser application that fulfills all your requirements. It uses
Tailwind CSSfor a clean, modern, professional interface, andVanilla JavaScriptwithlocalStorageso your tasks are saved locally in your browser and won’t vanish when you refresh.
Save the following code into a file named index.html on your computer, then simply double-click it to open it in any browser.
View/Copy HTML Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Work Task Manager</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-slate-50 text-slate-800 font-sans min-h-screen">
<div class="max-w-4xl mx-auto p-6">
<header class="flex justify-between items-center mb-8 border-b border-slate-200 pb-4">
<div>
<h1 class="text-3xl font-bold text-slate-900 tracking-tight">Task Manager</h1>
<p class="text-sm text-slate-500">Keep track of current and upcoming work tasks</p>
</div>
<div class="bg-slate-200 p-1 rounded-lg flex space-x-1">
<button id="viewActiveBtn" onclick="switchView('active')" class="px-4 py-1.5 text-sm font-medium rounded-md bg-white text-slate-900 shadow-xs transition-all">
Active Tasks
</button>
<button id="viewArchiveBtn" onclick="switchView('archive')" class="px-4 py-1.5 text-sm font-medium rounded-md text-slate-600 hover:text-slate-900 transition-all">
Archive
</button>
</div>
</header>
<main class="grid grid-cols-1 md:grid-cols-3 gap-8">
<section class="md:col-span-1 bg-white p-5 rounded-xl shadow-xs border border-slate-200 h-fit">
<h2 class="text-lg font-semibold text-slate-900 mb-4">Add New Task</h2>
<form id="taskForm" onsubmit="addTask(event)" class="space-y-4">
<div>
<label class="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1">Task Title *</label>
<input type="text" id="taskTitle" required placeholder="e.g., Review Q3 budget"
class="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm">
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1">Due Date *</label>
<input type="date" id="taskDate" required
class="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm">
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1">Notes</label>
<textarea id="taskNotes" rows="3" placeholder="Add details, links, or sub-tasks..."
class="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm resize-none"></textarea>
</div>
<button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-md text-sm transition-colors cursor-pointer shadow-xs">
Add Task
</button>
</form>
</section>
<section class="md:col-span-2 space-y-4">
<div class="relative">
<input type="text" id="searchBar" oninput="render()" placeholder="Search tasks by title or notes..."
class="w-full pl-4 pr-10 py-2.5 bg-white border border-slate-200 rounded-xl shadow-xs focus:outline-hidden focus:ring-2 focus:ring-indigo-500 text-sm">
<span class="absolute right-3 top-3 text-slate-400">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.604 10.604z" />
</svg>
</span>
</div>
<h2 id="listTitle" class="text-xl font-bold text-slate-900 px-1">Active Tasks</h2>
<div id="taskList" class="space-y-3">
</div>
</section>
</main>
</div>
<script>
// App State
let tasks = JSON.parse(localStorage.getItem('work_tasks')) || [];
let currentView = 'active'; // 'active' or 'archive'
// Save to localStorage
function saveTasks() {
localStorage.setItem('work_tasks', JSON.stringify(tasks));
}
// Add a new task
function addTask(event) {
event.preventDefault();
const title = document.getElementById('taskTitle').value.trim();
const dueDate = document.getElementById('taskDate').value;
const notes = document.getElementById('taskNotes').value.trim();
const newTask = {
id: Date.now(),
title,
dueDate,
notes,
completed: false,
completedAt: null
};
tasks.push(newTask);
saveTasks();
// Reset form
document.getElementById('taskForm').reset();
render();
}
// Toggle task status (Done / Undone)
function toggleTaskStatus(id) {
tasks = tasks.map(task => {
if (task.id === id) {
const completed = !task.completed;
return {
...task,
completed,
completedAt: completed ? new Date().toISOString() : null
};
}
return task;
});
saveTasks();
render();
}
// Delete task entirely (optional helper)
function deleteTask(id) {
if(confirm("Are you sure you want to permanently delete this task?")) {
tasks = tasks.filter(task => task.id !== id);
saveTasks();
render();
}
}
// Switch between Active and Archive views
function switchView(view) {
currentView = view;
const activeBtn = document.getElementById('viewActiveBtn');
const archiveBtn = document.getElementById('viewArchiveBtn');
const listTitle = document.getElementById('listTitle');
if (view === 'active') {
activeBtn.className = "px-4 py-1.5 text-sm font-medium rounded-md bg-white text-slate-900 shadow-xs transition-all";
archiveBtn.className = "px-4 py-1.5 text-sm font-medium rounded-md text-slate-600 hover:text-slate-900 transition-all";
listTitle.textContent = "Active Tasks";
} else {
archiveBtn.className = "px-4 py-1.5 text-sm font-medium rounded-md bg-white text-slate-900 shadow-xs transition-all";
activeBtn.className = "px-4 py-1.5 text-sm font-medium rounded-md text-slate-600 hover:text-slate-900 transition-all";
listTitle.textContent = "Archived Tasks";
}
render();
}
// Format dates cleanly for UI display
function formatDate(dateString) {
if (!dateString) return '';
const options = { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' };
return new Date(dateString).toLocaleDateString(undefined, options);
}
// Render the UI based on current state, filters, and sorting
function render() {
const taskListContainer = document.getElementById('taskList');
const searchQuery = document.getElementById('searchBar').value.toLowerCase();
taskListContainer.innerHTML = '';
// 1. Filter by Active vs Archived status
let filteredTasks = tasks.filter(task => {
return currentView === 'active' ? !task.completed : task.completed;
});
// 2. Filter by Search Query (checks title and notes)
if (searchQuery) {
filteredTasks = filteredTasks.filter(task =>
task.title.toLowerCase().includes(searchQuery) ||
task.notes.toLowerCase().includes(searchQuery)
);
}
// 3. Sort by Due Date (Closest/Today first)
// For the archive, you might prefer sorting by completion date, but keeping it consistent here.
filteredTasks.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate));
// Empty state notice
if (filteredTasks.length === 0) {
taskListContainer.innerHTML = `
<div class="text-center py-12 bg-white rounded-xl border border-dashed border-slate-200 text-slate-400 text-sm">
No tasks found matching this criteria.
</div>
`;
return;
}
// Render elements
filteredTasks.forEach(task => {
const isOverdue = !task.completed && new Date(task.dueDate) < new Date().setHours(0,0,0,0);
const taskCard = document.createElement('div');
taskCard.className = `p-4 bg-white rounded-xl border transition-all flex items-start space-x-3 shadow-xs hover:border-slate-300 ${task.completed ? 'opacity-70 border-slate-200' : 'border-slate-200'}`;
taskCard.innerHTML = `
<div class="pt-0.5">
<input type="checkbox" ${task.completed ? 'checked' : ''}
onclick="toggleTaskStatus(${task.id})"
class="w-4 h-4 rounded-sm text-indigo-600 focus:ring-indigo-500 border-slate-300 cursor-pointer transition-colors">
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between gap-2">
<h3 class="text-sm font-semibold text-slate-900 break-words ${task.completed ? 'line-through text-slate-400' : ''}">
${task.title}
</h3>
<span class="text-xs font-medium px-2 py-0.5 rounded-full shrink-0 ${task.completed ? 'bg-slate-100 text-slate-500' : isOverdue ? 'bg-rose-50 text-rose-600 font-semibold' : 'bg-indigo-50 text-indigo-600'}">
${formatDate(task.dueDate)} ${isOverdue ? ' (Overdue)' : ''}
</span>
</div>
${task.notes ? `<p class="text-xs text-slate-500 mt-1.5 whitespace-pre-wrap break-words leading-relaxed">${task.notes}</p>` : ''}
</div>
<button onclick="deleteTask(${task.id})" class="text-slate-300 hover:text-rose-500 p-0.5 rounded-md transition-colors shrink-0" title="Delete Permanently">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
</button>
`;
taskListContainer.appendChild(taskCard);
});
}
// Initial Load
render();
</script>
</body>
</html>
See the App Here
Now, obviously, that’s a pretty good response for a plain English prompt. It’s not perfect and I could refine it some more, but it does pretty much what I asked it to.
And that’s where AI coding shines, to be honest. If you can clearly describe what you want in a simple web app or dashboard, most coding agents (such as Claude Code, Gemini Flash Pro, or Codex) will get you pretty decent results.
But… pretty decent isn’t good enough when it comes to a production ready application with complexity. At least not if you want the app to be maintainable for the long run and avoid excess bugs.
Agentic Engineering
This is where having some knowledge of computer science principles, coding skills, and understanding software design (even from a high level) can be extremely helpful.
Knowing what variables are, what a loop is, what memory management is, etc is going to help you read and understand the code being written.
While understanding how API calls work or importing libraries or I/O standards, etc will also help you guide the tool more strategically when building out your plan.
You don’t need to be an expert with decades of experience, you just need a solid foundation. The good news is you can always ask the AI for help understanding a concept if you’re struggling to do so and it’ll likely give you a solid answer - just be sure to do your own research to back it up.
That being said, I view agentic engineering as a sort of mature version of vibe coding. Instead of simply giving one prompt and expecting an output, then having to re-prompt time and again as you iterate through issues, you structure documentation for your agent to follow (PRD doc, MoSCoW doc, Features docs, Branding doc, Architecture doc, Memory doc, etc).
Then, as you build, tell the agent to reference a specific doc, which ideally you’ve written with very thorough instructions and pointers to other docs it should reference.
For instance you might say:
Reference /docs/features/feature_1.md and follow the brief.
Ideally, if you have built your documentation correctly, the agent will get all the context needed to build that feature and do it well. You may need to iterate some on the output and that’s to be expected, but in general it should build that feature within all your guidelines very well.
Then you move on to the next feature and the next and the next after that.
If you can manage to do those things well, then in theory, you should have a much better time creating production ready applications with agentic coding tools.
[!Important] The bottom-line is that you must think through the app design in advance and build out the full architecture scaffolding, design language, and documentation in order to guide the agent as thoroughly as possible.