50% off SaaS Starter Kit — only for the first 100 buildersGrab it →
← Back to blog
vibecodingMay 21, 2026·8 min read

Prompt Engineering for Code Generation: What Actually Works

Skip the theory. Here's how we prompt AI to write code we actually ship, not code we spend an hour fixing.

Ștefan Binisor

Ștefan Binisor

Co-founder, peal.dev

Prompt Engineering for Code Generation: What Actually Works

We've been using AI code generation daily for over a year now. Claude, GPT-4, Cursor, the whole gang. And the gap between a prompt that gives you something usable on the first try versus one that gives you a plausible-looking mess is almost entirely about how you ask. Not which model. Not which tool. How you ask.

This isn't about being nice to the AI or adding magic words like 'think step by step'. That stuff has its place, but most prompt engineering guides for code generation are written by people who don't actually ship software for a living. This is what works for us — two developers building a Next.js template marketplace — when we need real, working code.

Give Context Like You're Onboarding a Developer, Not Typing a Search Query

The biggest mistake we see — and made ourselves for months — is treating AI like a search engine. 'How do I do X in Next.js?' gets you a generic answer. What you actually need is to treat the AI like a contractor who just joined the project. They need context to not make decisions you'll hate.

A bad prompt: 'Write a function to handle authentication.' A good prompt tells the contractor what they need: what stack you're on, what you already have, what constraints exist, and what done looks like.

// Bad prompt
Write a function to handle user authentication.

// Good prompt
I'm building a Next.js 14 app using the App Router.
I'm using Drizzle ORM with a PostgreSQL database.
I already have a `users` table with columns: id, email, password_hash, created_at.
I'm using bcrypt for password hashing (already installed).

Write a `validateUserCredentials(email: string, password: string)` function that:
- Queries the users table by email
- Compares the password against password_hash using bcrypt.compare
- Returns the user object (without password_hash) on success, or null on failure
- Uses TypeScript with proper types
- Does NOT use try/catch — I handle errors at the call site

Use this Drizzle db instance: import { db } from '@/lib/db'

That second prompt is longer. It's also going to save you 20 minutes of edits. The time you spend writing a better prompt is almost always less than the time you spend fixing a bad output.

Specify Your Constraints Explicitly — Especially the Negative Ones

AI models want to be helpful. That means they'll often add things you didn't ask for: error boundaries, console.log statements, TODO comments, their own opinions on your architecture. Left to their own devices, they'll wrap everything in try/catch, add logging you don't want, and sometimes even refactor code you didn't ask them to touch.

Negative constraints — telling the AI what NOT to do — are some of the most valuable tokens you can spend in a prompt.

  • Do NOT add console.log or console.error statements
  • Do NOT wrap in try/catch — error handling is done at the caller
  • Do NOT add JSDoc comments
  • Do NOT refactor existing code I haven't asked you to change
  • Do NOT add default export — I need a named export
  • Do NOT use any — this is a strict TypeScript codebase
  • Do NOT add 'use client' unless I specifically ask for it

We keep a short list of our project's standard negative constraints in a CLAUDE.md file (Cursor's equivalent of a system prompt). It took us embarrassingly long to realize we could just... tell the AI our preferences once, permanently.

Show, Don't Just Tell — Paste Your Existing Code

If you want the AI to write something that looks like your codebase, show it your codebase. This sounds obvious but most people describe their code instead of pasting it. Describing creates ambiguity. Pasting leaves no room for interpretation.

When we need a new Drizzle schema, we paste an existing schema file. When we need a new Server Action, we paste an existing one. The model then picks up on our naming conventions, our export style, our error handling patterns — all the things you'd otherwise need to describe in 200 words.

Here's an existing schema file from my project:

```typescript
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'

export const organizations = pgTable('organizations', {
  id: uuid('id').primaryKey().defaultRandom(),
  name: text('name').notNull(),
  slug: text('slug').notNull().unique(),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
})

export type Organization = typeof organizations.$inferSelect
export type NewOrganization = typeof organizations.$inferInsert
```

Following the exact same patterns (naming, types, exports), write a `subscriptions` table with these columns:
- id (uuid, primary key)
- organizationId (uuid, foreign key to organizations)
- stripeSubscriptionId (text, unique)
- status (text — values will be: 'active' | 'canceled' | 'past_due' | 'trialing')
- currentPeriodEnd (timestamp)
- createdAt, updatedAt (same as above)

The output from this prompt will slot into your codebase without edits. Compare that to describing your schema style in prose and hoping the model guesses right.

Ask for One Thing at a Time (Seriously)

There's a temptation to mega-prompt. 'Write me the schema, the server actions, the form component, and the API route for this feature.' We've done this. The output is always a tangled mess that's harder to review than it is to just write yourself.

Complex tasks done in one shot tend to have subtle bugs in the middle sections — the parts where the AI is context-switching between different concerns. The schema at the top is good. The API route at the bottom is good. The server action in the middle has a wrong import and a type mismatch that'll take you 10 minutes to spot.

Break it up. Schema first, review it, paste it as context for the server actions, review those, paste them as context for the component. Each step is 30 seconds of prompting. Each step produces something you can actually verify before moving to the next.

Treat AI code generation like building with Lego: assemble verified pieces one at a time, not pour a mold and hope the whole thing comes out right.

Use the 'What Would Break This' Follow-Up

This is probably the most underused prompt technique we know, and we stumbled onto it after a particularly painful 2am deploy where AI-generated code failed on a race condition we should have caught. After getting code you're happy with, ask:

What are the edge cases or failure modes in the code you just wrote?
Don't rewrite anything — just list what could go wrong and under what conditions.

The model will often surface things like: 'This will fail if the user has no active subscription', 'This assumes the webhook payload is always valid JSON', 'This doesn't handle the case where the database insert succeeds but the email send fails'. Things it knew about but didn't mention unless you asked.

It's not always right — sometimes it invents problems that don't exist. But it's right often enough that we do this for any non-trivial code before shipping. It's like having a code reviewer who never gets tired of reading the same function.

When AI Gets It Wrong: Guided Correction vs. Retry

When the output isn't what you wanted, the instinct is to just regenerate. That's usually wrong. A retry with no additional information gives you a slightly different version of the same misunderstanding. Instead, diagnose what specifically went wrong and correct just that.

// Instead of: [regenerate]

// Do this:
The function is close but there are two issues:
1. You're using `findOne` which doesn't exist in Drizzle — use `db.select().from(table).where(...).limit(1)` instead
2. The return type should be `Promise<User | null>` not `Promise<User | undefined>`

Fix only these two issues, don't change anything else.

That 'don't change anything else' at the end is important. Without it, the model will sometimes decide to 'improve' other parts of the code while fixing your issue. Then you're reviewing a diff when you only asked for a small fix.

Guided correction also teaches the AI (within the context window) what your project looks like, making subsequent outputs in the same conversation better. A fresh retry throws that away.

Build a Prompt Library, Not Just a Snippet Library

We have a folder called `prompts/` in our internal tooling repo. Not code snippets — actual prompt templates for recurring tasks. New Drizzle migration. New Stripe webhook handler. New email template with React Email. New Server Action with optimistic updates.

Each template has the context pre-filled (our stack, our conventions, our imports), the negative constraints listed, and a placeholder for the specific thing we need. When we need that thing, we fill in the placeholder and we're done. Generating the 10th webhook handler takes 30 seconds instead of 3 minutes.

This is also part of the thinking behind peal.dev — our Next.js templates come with enough working code that you can paste real examples into your prompts right away, instead of starting from a bare scaffold where you have nothing to show the AI. Context-rich templates make AI generation significantly better from day one.

The Checklist Before You Send

We've made enough bad prompts that we now do a quick mental checklist before hitting send on anything non-trivial:

  • Did I specify the framework/library version? (Next.js 14 App Router, not just 'Next.js')
  • Did I paste existing code for the AI to pattern-match against?
  • Did I list what I DON'T want?
  • Did I define what 'done' looks like? (function signature, return type, export style)
  • Am I asking for one thing, or secretly asking for five?
  • Do I have the relevant imports/types in the context the AI can see?

Six questions, takes 10 seconds. Saves a lot of back-and-forth.

The best prompt isn't the longest one. It's the one that leaves the least room for the model to guess about what you actually need.

Prompt engineering for code is really just communication skills applied to a weird context. Be specific, give examples, say what you don't want, verify each step. The same things that make you a good collaborator with other developers make you effective at getting usable code out of AI. The difference is the AI never tells you when your requirements are unclear — it just silently makes assumptions and hands you something that compiles but doesn't do what you meant.

That's on us to close the gap.

Newsletter

Liked this post? There's more where it came from.

Dev guides, honest build stories, and the occasional 2am debugging confession — straight to your inbox. No spam, unsubscribe anytime.

Browse templates
Written by humansWeekly dropsSubscriber perks

Join the Discord

Ask questions, share builds, get help from founders