For the last month, I’ve been experimenting with vibe coding on a couple of side projects. I’ve found that not only is it remarkably good, but it’s also a practice you can get measurably better at if you’re open to tinkering and picking up best practices. In this article, I want to share some ways you can get great results when vibe coding.
It’s like prompt engineering from a year or two ago. People were discovering new stuff every week and posting about it on social media. The best techniques are the same techniques that a professional software engineer might use. Some might argue that this is just software engineering, not vibe coding. I think that’s beside the point. We’re trying to use these tools to get the best results.
Before I give my advice for Vibe Coding, let’s hear from founders on the tips they’re using to get the best out of the AI tools today.
Tips from the Trenches: Founder Advice
- Escape the Loop: “If you get stuck in a place where the AI IDE can’t implement or debug something and it’s just stuck in a loop, sometimes going to the LLM’s website, like literally to the UI, and just pasting in your code and asking the same question can get you a result that for whatever reason the IDE couldn’t get to.”
- Dual-Wield AIs: “Load up both Cursor and Windsurf on the same project. Cursor is a bit faster, so you can do a lot of the front-end work. Windsurf thinks for a bit longer. Now, while I’m waiting for Windsurf to think, I can go on Cursor and start updating the front end. Sometimes I’ll give them the same prompt and they’ll both give me slightly different iterations. I’ll just pick which one I like better.”
- Treat AI as a New Language: “Think of the AI as a different programming language and vibe coding as a new type of programming. Instead of programming with code, you’re programming with language. Because of that, you have to provide a lot of the necessary context and information in a very detailed way if you want to get good results.”
- Test-Driven Vibe Coding: “I usually start vibe coding in the reverse direction, starting from the test cases. I handcraft my test cases. I don’t use any LLMs to write them. Once it is done, I have strong guardrails that my LLMs can follow for generating the code. Once I see those green flags on my test cases, the job is done.”
- Architect Before You Code: “It’s very important to first spend an unreasonable amount of time in a pure LLM to build out the scope and the actual architecture of what you’re trying to build before offloading that to a coding tool and letting it just free-run in the codebase.”
- Spot the Rabbit Hole: “Really monitor whether the LLM is falling into a rabbit hole. If you notice that it just keeps regenerating funky-looking code and you’re copy-pasting error messages all the time, it probably means something’s gone awry. Take a step back and prompt the LLM, ‘Hey, let’s take a step back and try to examine why it’s failing.’”
The overarching theme here is to make the LLM follow the processes that a good professional software developer would use. So, let’s dive in and explore some of the best vibe coding advice I’ve seen.
Where to Start: Choosing Your Tools
If you’ve never written any code before, I would probably go for a tool like Replit or Lovable. They give you an easy-to-use visual interface and it’s a great way to try out new UIs directly in code. Many product managers and designers are going straight to implementation of a new idea in code rather than designing mock-ups in something like Figma, just because it’s so quick.
However, when I tried this, I was impressed with the UIs, but tools like Lovable started to struggle when I wanted to more precisely modify backend logic. I’d change a button, and the backend logic would bizarrely change.
So, if you’ve written code before, even if you’re a little bit rusty, you can probably leap straight to tools like Windsurf, Cursor, or Claude Code.
The Power of Planning
Once you’ve picked your tool, the first step is not to dive in and write code. Instead, work with the LLM to write a comprehensive plan. Put that in a markdown file inside your project folder and keep referring back to it. This is a plan that you develop with the AI and you step through while you’re implementing the project, rather than trying to one-shot the whole thing.
After you’ve created the first draft of this plan, go through it. Delete or remove things you don’t like. You might mark certain features explicitly as “won’t do” because they’re too complicated. You might also like to keep a section of “ideas for later” to tell the LLM, “Look, I considered this, but it’s out of scope for now.”
Once you’ve got that plan, work with the LLM to implement it section by section. Explicitly say, “Let’s just do section two right now.” Then you check that it works, you run your tests, and you git commit. Then have the AI go back to your plan and mark section two as complete.
I probably wouldn’t expect the models to one-shot entire products yet, especially if they’re complicated. I prefer to do this piece by piece, ensuring I have a working implementation of each step. Crucially, commit it to Git so that you can revert if things go wrong. But honestly, this advice might change in the next few months. The models are getting better so quickly that it’s hard to say where we’re going to be.
Version Control is Your Best Friend
Use Git religiously. I know the tools have revert functionality, but I don’t trust them yet. So, I always make sure I’m starting with a clean Git slate before I start a new feature so that I can revert to a known working version if the AI goes off on a vision quest.
Don’t be afraid to run git reset --hard HEAD if it’s not working and just roll the dice again. I found I had bad results if I’m prompting the AI multiple times to try to get something working. It tends to accumulate layers of bad code rather than understanding the root cause. You might try five or six different prompts and finally get the solution. I’d just take that solution, reset, and then feed that solution into the AI on a clean codebase.
The Importance of Testing
The next thing you should do is write tests or get your LLM to write tests for you. They’re pretty good at this, although they often default to writing very low-level unit tests. I prefer to keep these tests super high-level. You want to simulate someone clicking through the site or the app and ensure that the features are working end-to-end, rather than testing functions on a unit basis. Make sure you write high-level integration tests before you move on to the next feature.
LLMs have a bad habit of making unnecessary changes to unrelated logic. You tell it to fix one thing, and it changes some logic over here for no reason at all. Having these test suites in place catches these regressions early and will identify when the LLM has gone off and made unnecessary changes so that you can reset and start again.
Beyond Coding: AI as Your DevOps and Designer
Keep in mind LLMs aren’t just for coding. I use them for a lot of non-coding work. For example, I had an AI configure my DNS servers—a task I always hated—and set up Heroku hosting via a command-line tool. It was a DevOps engineer for me and accelerated my progress tenfold.
I also used an AI to create an image for my site’s favicon. Then, another AI took that image and wrote a quick throwaway script to resize it into the six different sizes and formats I needed. The AI is now my designer as well.
Here’s a simple Python script to demonstrate how you could automate resizing favicons:
from PIL import Image
import os
def create_favicons(source_path, output_folder):
"""
Generates multiple favicon sizes from a single source image.
"""
if not os.path.exists(output_folder):
os.makedirs(output_folder)
sizes = [16, 32, 48, 64, 128, 192, 256]
try:
with Image.open(source_path) as img:
for size in sizes:
resized_img = img.resize((size, size), Image.LANCZOS)
output_path = os.path.join(output_folder, f"favicon-{size}.png")
resized_img.save(output_path)
print(f"Generated {output_path}")
except FileNotFoundError:
print(f"Error: The file at {source_path} was not found.")
except Exception as e:
print(f"An unexpected error occurred: {e}")
# --- How to use it ---
# create_favicons("path/to/your/logo.png", "dist/favicons")
Debugging with AI
The first thing I do when I encounter any bug is just copy-paste the error message straight back into the LLM. It might be from your server log files or the JavaScript console. Often, this error message is enough for the AI to identify and fix the problem. You don’t even need to explain what’s going wrong.
This is so powerful that I expect all major coding tools will soon be able to ingest these errors without humans having to copy-paste. Our value as the copy-paste machine is weird. That part of the process will go away as LLM tools learn to tail logs or spin up a headless browser to inspect JavaScript errors directly.
With more complex bugs, ask the LLM to think through three or four possible causes before writing any code. After each failed attempt, git reset and start again so you’re not accumulating layers of crust. If in doubt, switch models. I often find that different models succeed where others fail.
If you do eventually find the source of a gnarly bug, reset all the changes and then give the LLM very specific instructions on how to fix that precise bug on a clean codebase.
Instructing Your AI Agent
Write instructions for the LLM. Put these instructions in whatever format your tool uses—Cursor rules, Windsurf rules, or a markdown file. Each tool has a slightly different naming convention. I know founders who’ve written hundreds of lines of instructions for their AI coding agent, and it makes them way more effective. There’s tons of advice online about what to put in these instruction files.
Handling Documentation
I still find that pointing these agents at online web documentation is a little patchy. Some people suggest using an MCP server to access documentation, which seems like overkill to me. I’ll often just download all of the documentation for a given set of APIs and put them in a subdirectory of my working folder so the LLM can access them locally. Then, in my instructions, I’ll say, “Go and read the docs before you implement this thing.” It’s often much more accurate.
Side Note: You can use the LLM as a teacher. You might implement something and then get the AI to walk through that implementation line by line and explain it to you. It’s a great way to learn new technologies, much better than scrolling Stack Overflow.
Tackling Complex Features
If you’re working on a new feature that’s more complex than you’d normally trust the AI to implement, do it as a standalone project in a totally clean codebase. Get a small reference implementation working without the complication of your existing project, or even download one from GitHub. Then, point your LLM at that implementation and tell it to follow that while reimplementing it inside your larger codebase. It works surprisingly well.
Architecture Matters: Modularity is Key
Small files and modularity are your friends. This is true for human coders as well. I think we might see a shift towards more modular or service-based architectures where the LLM has clear API boundaries it can work within. Huge monorepos with massive interdependencies are hard for both humans and LLMs. With a modern architecture and a consistent external API, you can change the internals as long as the external interface and the tests still pass.
Choosing the Right Tech Stack
I chose to build my project partially in Ruby on Rails, and I was blown away by the AI’s performance. I think this is because Rails is a 20-year-old framework with a ton of well-established conventions. A lot of Rails codebases look very similar, which means there’s a ton of pretty consistent, high-quality training data online. I’ve had friends have less success with languages like Rust or Elixir where there’s just not as much training data, but that might change very soon.
Advanced Interaction: Screenshots and Voice
You can copy and paste screenshots into most coding agents these days. It’s very useful either to demonstrate a bug in the UI or to pull in design inspiration from another site.
Voice is another really cool way to interact with these tools. Using a transcription tool, I can effectively input instructions at 140 words per minute, which is about double what I can type. The AI is so tolerant of minor grammar and punctuation mistakes that it honestly doesn’t matter if the transcription isn’t perfect.
Refactor, Refactor, Refactor
When you’ve got the code working and the tests implemented, you can refactor at will, knowing that your tests will catch any regressions. You can even ask the LLM to identify parts of your codebase that seem repetitive or might be good candidates for refactoring. Again, this is just a tip that any professional software developer would follow. Keep files small and modular.
Finally, Keep Experimenting
The state-of-the-art of this stuff changes week by week. I try every new model release to see which performs better. Some are better at debugging, long-term planning, or implementing features. For example, at the moment, Gemini seems best for whole-codebase indexing and planning, while Sonet 3.7 seems like a leading contender to implement the code changes. I tried GPT-4.1 a couple of days ago and wasn’t as impressed, but I’ll try it again next week. I’m sure things will have changed again.