Skip to content
Lesson 5 of 22

Thinking in Prompts

20 min read

Why Prompting is THE Skill

If vibe coding is the new way to build software, then prompting is the new way to write it. Your prompt quality directly determines your output quality. There is no getting around this — a vague prompt produces vague code, and a precise prompt produces precise code.

Think about it this way: when you hire a contractor to remodel your kitchen, the instructions you give them matter. "Make it nice" gets you something very different from "Install white shaker cabinets, quartz countertops, and a farmhouse sink with brushed nickel fixtures." The contractor might be incredibly skilled, but without clear direction, they are guessing at what you want.

AI coding tools work the same way. Claude Code, Cursor, and every other AI assistant are extraordinarily capable — but they need you to communicate clearly. The tool isn't the bottleneck. You are.

This might sound harsh, but it's actually empowering. It means you can dramatically improve your results without learning a single programming concept. You just need to get better at describing what you want.

Prompting is the new "typing." In the 1990s, typing speed was a legitimate job skill. People took courses to improve their words-per-minute. Today, nobody lists typing on their resume — it's assumed. Prompting is on the same trajectory. Right now, it's a differentiator. In five years, it will be a baseline expectation. The people who master it now will have a significant head start.

The Prompt Quality Hierarchy

Not all prompts are created equal. Let's look at the same task — adding a contact form to a website — at five different quality levels. Watch how the output improves as the prompt gets more specific.

Level 1: Vague

Add a contact form

This tells the AI almost nothing. What kind of form? Where does it go? What fields does it need? Where should submissions go? The AI will guess at every decision, and its guesses probably won't match what you have in mind. You'll spend more time fixing the output than you saved by being brief.

Level 2: Basic

Add a contact form to the homepage with name, email, and message fields

Better. Now the AI knows where the form goes and what fields to include. But there are still a lot of unanswered questions. What happens when someone submits? Is there validation? What does it look like?

Level 3: Good

Add a contact form to the homepage with name, email, and message fields.
Use a server action to handle submission and send the data to my email.
Show a success message after submission.

Now we are getting somewhere. The AI knows the fields, the submission mechanism, and the expected user flow. This will produce a working form in most cases.

Level 4: Great

Add a contact form section to the homepage, below the testimonials section.
Fields: name (required), email (required, validated), message (required, textarea).
Use a Next.js server action in app/actions/contact.ts to handle submission.
Validate all fields server-side. Send the form data to my email using Resend.
Show a loading spinner on the submit button during submission.
On success, show a green toast notification "Message sent! We'll reply within 24 hours."
On error, show a red toast notification with the error message.
Style it to match the existing sections — use the same padding, max-width, and font styles.

This is a prompt that will produce production-ready code on the first try. The AI knows the location, the fields with validation rules, the tech stack, the error handling, and the styling expectations.

Level 5: Excellent

Add a contact form section to the homepage (app/page.tsx), positioned between
the testimonials and footer sections.

## Form Fields
- Name: text input, required, min 2 characters
- Email: email input, required, validated with a proper email regex
- Message: textarea, required, min 10 characters, max 1000 characters
- Show character count below the message field

## Submission
- Create a server action in app/actions/contact.ts
- Validate all fields server-side (never trust client-side only)
- Use Resend (already installed) to send to contact@mysite.com
- Rate limit: max 3 submissions per IP per hour using the existing
  rate limiter in lib/rate-limit.ts

## UX
- Disable the submit button and show a spinner during submission
- On success: green toast "Message sent! We'll reply within 24 hours."
  and reset the form
- On error: red toast with the error message, keep the form data intact
- Add client-side validation that shows inline errors below each field
  as the user types (after first blur)

## Style
- Match the existing section pattern: py-24 px-6, max-w-3xl mx-auto
- Use the existing form input styles from the newsletter signup component
- The submit button should use the primary CTA style (bg-blue-600)

At this level, you are thinking like a product owner. Every decision is made upfront, references to existing patterns are explicit, edge cases are covered, and the AI has essentially zero ambiguity. The result will be exactly what you want, and you probably won't need a single follow-up prompt.

The lesson here is not that every prompt needs to be this detailed. Quick changes deserve quick prompts. But for important features, the time you invest in the prompt pays for itself many times over.

The Role of Context

Humans are great at filling in gaps. When your coworker says "fix the button," you know which button because you were just discussing it. AI doesn't have that shared context unless you provide it.

Context is the information surrounding your request that helps AI make the right decisions. Without it, AI has to guess — and guessing leads to rewrites.

What Context to Provide

Project type: "This is a SaaS dashboard" vs. "This is a marketing landing page" leads to completely different design decisions. A dashboard uses data tables and sidebar navigation. A landing page uses hero sections and call-to-action buttons.

Tech stack: Mentioning your framework changes everything. "Add authentication" produces generic code. "Add authentication using NextAuth.js with the GitHub provider in a Next.js App Router project" produces code you can actually use.

Existing patterns: "Follow the same pattern as the UserProfile component" is incredibly powerful. It tells AI to look at existing code and match its style, structure, and conventions. This is how you maintain consistency across a project.

User expectations: "The form should work like the Stripe checkout form — clean, minimal, with inline validation" paints a picture that's worth a thousand words of technical specification.

Constraints: "We're using the free tier of Supabase, so keep database queries efficient" or "This needs to work without JavaScript for SEO purposes" helps AI make the right tradeoffs.

The more context you provide, the less the AI has to guess. And every guess is a potential mismatch with what you actually wanted.

The "Describe the User Experience" Technique

Here is one of the most powerful prompting techniques for vibe coding: instead of describing what the code should do, describe what the user sees and does.

This works because AI is excellent at translating user experiences into implementations. When you describe the UX, the AI figures out the technical details. When you describe the implementation, you have to know the technical details yourself.

Example 1: Authentication

Implementation-focused prompt (harder for non-developers):

Create a middleware that checks for a JWT token in the cookies,
validates it against the JWKS endpoint, and redirects to /login
if the token is expired or invalid. Set up the auth context provider
to wrap the app and expose the user object.

UX-focused prompt (anyone can write this):

When a user visits any page, check if they're logged in.
If they're not logged in, send them to the login page.
The login page has email and password fields, a "Sign In" button,
and a "Forgot password?" link.
After signing in, take them back to the page they originally tried to visit.
Their name should appear in the top-right corner of every page.

Both prompts can produce working authentication. But the second one is accessible to anyone, and it actually provides more useful information about the desired user flow.

Implementation-focused:

Add a search endpoint that accepts a query parameter, uses full-text
search on the posts table with ts_vector, and returns paginated results
with highlighted matches.

UX-focused:

Add a search bar at the top of the blog page. As the user types,
show a dropdown with matching post titles (debounced, after 300ms).
Clicking a result goes to that post. Pressing Enter shows a full
search results page with the matching text highlighted in yellow.
Show "No results found" with a suggestion to try different keywords
if nothing matches.

Example 3: Notifications

Implementation-focused:

Set up a WebSocket connection that listens for notification events,
stores them in a Redux slice, and renders a badge component with
an unread count.

UX-focused:

Add a bell icon in the header that shows a red badge with the number
of unread notifications. Clicking the bell opens a dropdown showing
the 10 most recent notifications with the sender's avatar, the message,
and how long ago it was sent (like "2 hours ago"). Unread ones have
a blue dot. Clicking a notification marks it as read and takes you to
the relevant page. There's a "Mark all as read" link at the bottom.

The pattern is clear: describe the screens, the interactions, the visual feedback, and the edge cases — and let AI handle the technical implementation.

Prompt Patterns for Common Tasks

Here are battle-tested prompt patterns for the tasks you'll perform most often. Each shows the difference between a prompt that will cause friction and one that will produce clean results.

Adding a New Feature

Weak: Add dark mode

Strong:

Add a dark mode toggle to the site header, next to the navigation links.
Use a sun/moon icon that switches on click. Store the preference in
localStorage so it persists across visits. Default to the system preference
on first visit. Use CSS variables for the color scheme so all existing
components automatically respect the theme. Add the toggle to the
MobileMenu component as well.

Fixing a Bug

Weak: The form is broken

Strong:

The contact form on /contact throws this error when submitted:
"TypeError: Cannot read property 'email' of undefined"
The form was working before I added the phone number field in the last commit.
The error is in app/actions/contact.ts. The form data is probably not being
parsed correctly after adding the new field.

Refactoring Code

Weak: Clean up this code

Strong:

Refactor the user dashboard page (app/dashboard/page.tsx). It's currently
450 lines in a single file. Extract the stats cards into a StatsGrid
component, the activity feed into an ActivityFeed component, and the
chart section into a UsageChart component. Put them in
components/dashboard/. Keep the data fetching in the page component
and pass data as props.

Styling and UI Changes

Weak: Make it look better

Strong:

Update the pricing page cards to match this design:
- Three cards in a row on desktop, stacked on mobile
- The middle card (Pro plan) should be slightly larger and have a
  "Most Popular" badge
- Each card has: plan name, price with /month, a list of features
  with checkmark icons, and a CTA button
- Use the existing color palette — primary blue for the Pro card,
  neutral gray for the others
- Add a subtle hover effect: slight scale up and shadow increase

Adding a New Page

Weak: Add an about page

Strong:

Create an About page at app/about/page.tsx with these sections:
1. Hero with headline "About Us" and a short paragraph about our mission
2. Team section with a grid of team member cards (photo, name, role, bio)
   — use placeholder data for 4 team members
3. Values section with 3 columns: Innovation, Transparency, Community
   — each with an icon and short description
4. CTA section at the bottom: "Want to join us?" with a link to /careers
Use the same layout wrapper as the existing pages.
Add the page to the main navigation.

Setting Up a New Project

Weak: Start a new project

Strong:

Create a new Next.js 14 project with App Router, TypeScript, and Tailwind CSS.
Set up the following:
- ESLint and Prettier with the default Next.js config
- A basic layout with header, main, and footer
- A homepage with a hero section placeholder
- Environment variables file (.env.local) with placeholder values for
  DATABASE_URL and NEXT_PUBLIC_SITE_URL
- A .gitignore that covers Node.js, Next.js, and environment files
- Initialize git with an initial commit

Writing Tests

Weak: Add tests

Strong:

Write tests for the auth utilities in lib/auth.ts using Vitest.
Test these scenarios:
- validateEmail returns true for valid emails, false for invalid
- hashPassword produces different hashes for different passwords
- verifyPassword returns true for correct password, false for incorrect
- generateToken creates a valid JWT with the expected payload
- Token expiry: a token created with 1-hour expiry should fail
  validation after that period
Use descriptive test names that explain the expected behavior.

API Integration

Weak: Add Stripe

Strong:

Integrate Stripe for subscription payments:
1. Create a checkout API route at app/api/checkout/route.ts that
   creates a Stripe Checkout Session for the selected plan
2. Create a webhook handler at app/api/webhooks/stripe/route.ts
   that listens for checkout.session.completed and
   customer.subscription.updated events
3. When a subscription is created, update the user's plan field
   in the database
4. Add a "Subscribe" button to the pricing page that calls the
   checkout API and redirects to Stripe
5. Use the STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET from .env.local

Framework-Specific Prompt Patterns

Generic prompts produce generic code. When you mention your specific framework and its conventions, AI generates code that actually fits your project.

Next.js App Router

Instead of "add a page," say:

Create a server component at app/blog/[slug]/page.tsx that fetches
the blog post by slug using the existing getBlogPost function from
lib/blog.ts. Generate static params using getAllBlogSlugs.
Add metadata generation using generateMetadata for SEO.

Mentioning "server component," "App Router," "generateMetadata," and "generateStaticParams" tells the AI exactly which Next.js patterns to use.

React Components

Instead of "make a modal component," say:

Create a reusable Modal component in components/ui/Modal.tsx.
It should use a React portal to render outside the main DOM tree.
Accept children, isOpen, onClose, and title as props.
Close on Escape key and outside click. Trap focus inside the modal
for accessibility. Use Framer Motion for enter/exit animations.

Tailwind CSS

Instead of "style the card," say:

Style the card component using Tailwind CSS. Use rounded-xl for
border radius, shadow-md with hover:shadow-lg for depth, p-6 for
padding. Add a ring-1 ring-gray-200 border. Make it responsive:
full width on mobile, half width on tablet (md:w-1/2), third width
on desktop (lg:w-1/3). Use the dark: variant for dark mode support.

API Routes

Instead of "create an API," say:

Create a Next.js Route Handler at app/api/users/route.ts.
Implement GET (list users with pagination) and POST (create user).
Use Zod for request body validation on POST. Return proper HTTP
status codes: 200 for success, 400 for validation errors, 401 for
unauthorized, 500 for server errors. Each error response should
include a message field explaining what went wrong.

Iterative Refinement

Here is a truth about vibe coding: your first prompt rarely produces the perfect result. And that's completely fine. The magic is in the iteration.

Iteration is not failure. It's the process. Even expert prompt engineers iterate. The difference between beginners and experts isn't that experts get it right the first time — it's that experts iterate faster and more precisely.

How to Give Follow-Up Feedback

After the AI generates code, you'll try it out and notice things you want to change. The key to effective follow-up is specificity.

Vague follow-up: That's not right, try again

This tells the AI nothing. It might change something random, or it might start over from scratch and lose the parts that were working.

Specific follow-up:

The form layout looks good, but two things need fixing:
1. The submit button should be full width on mobile but auto-width on desktop
2. The success message is showing below the form — it should replace
   the form entirely so the user doesn't see the empty fields

The Art of "Almost, But..."

Most follow-ups fall into the "almost, but" category. The code is 80% right, and you need to guide it to 100%. Here is the formula:

[What's working] + [What's not working] + [What I want instead]

Examples:

The animation is smooth but it starts too abruptly. Add a 200ms delay
before the fade-in starts so the page has time to load.
The data table is displaying correctly, but the columns are too wide
on mobile. Make the email column hidden on screens smaller than 768px
and truncate the name column to 20 characters with an ellipsis.
The API route is working, but it returns all fields including the
password hash. Add a select statement to only return id, name, email,
and createdAt.

Each of these tells the AI exactly where to look and what to change. No guessing required.

Anti-Patterns That Waste Time

Certain prompting habits consistently lead to poor results. Recognizing these anti-patterns will save you hours of frustration.

1. Being Too Vague

"Make it better" or "fix the styling" gives AI no direction. Always specify what "better" means to you.

2. Providing Contradictory Requirements

"Make the design minimal and clean, but also add animations, gradients, shadows, icons for every item, and a particle background." The AI will try to satisfy everything and the result will be a mess.

3. Changing Requirements Mid-Prompt

Starting with "build a simple contact form" and halfway through the same prompt adding "actually, make it a multi-step wizard with file uploads and CRM integration." Break these into separate, sequential prompts.

4. Asking for Too Many Things at Once

"Add authentication, a dashboard, a billing system, and an admin panel." Each of these is a significant feature. Tackle them one at a time so you can test and validate each one before moving on.

5. Not Providing Error Messages

"It's broken" is not actionable. Always paste the actual error message, the URL where it occurs, and the steps to reproduce.

6. Assuming AI Remembers Everything

In a long conversation, AI's attention to earlier messages fades. If you're referencing a decision from 30 messages ago, restate it.

7. Over-Specifying Implementation Details

If you don't know the technical details, don't guess. "Use a useReducer with a dispatch pattern for state management" might be wrong for your use case. Describe the behavior and let AI choose the implementation.

8. Ignoring What Already Exists

"Create a button component" when you already have one leads to duplicated code. Always mention: "Use the existing Button component from components/ui/Button" or "Check if a similar component already exists before creating a new one."

9. Writing Prompts in a Wall of Text

Structure your prompts with line breaks, headers, bullet points, and numbered lists. AI parses structured input much better than paragraphs.

10. Not Checking the Output Before Continuing

If the first change introduced a bug and you keep building on top of it, you'll create a chain of issues that's hard to untangle. Test after every significant change.

Exercises: Practice Your Prompts

The best way to improve your prompting is to practice. Here are three scenarios. Try writing a prompt for each one before looking at the suggested answer.

Scenario 1: E-Commerce Product Page

You're building an online store. You need a product detail page that shows a product image, title, price, description, size selector, and an "Add to Cart" button. The store uses Next.js with Tailwind CSS.

Pause and write your prompt before reading the suggestion.

Suggested prompt:

Create a product detail page at app/products/[id]/page.tsx.

The page should display:
- A large product image on the left (or top on mobile)
- On the right side: product title (h1), price in bold, a short
  description paragraph, a size selector dropdown (S, M, L, XL),
  quantity selector (1-10), and an "Add to Cart" button
- Below the main section: a tabbed area with "Description,"
  "Reviews," and "Shipping" tabs

Fetch the product data using the getProduct(id) function from lib/products.ts.
Use Next.js generateMetadata for the page title and description.
The "Add to Cart" button should call the addToCart server action
and show a toast confirmation.
Make the layout responsive: side-by-side on desktop (lg:), stacked on mobile.
Style using Tailwind — clean, modern, lots of white space.

Scenario 2: User Dashboard

You need a dashboard that shows the user's account overview: recent activity, subscription status, and usage statistics.

Pause and write your prompt before reading the suggestion.

Suggested prompt:

Create a user dashboard at app/dashboard/page.tsx (protected route,
redirect to /login if not authenticated).

Layout: sidebar navigation on the left with links to Dashboard,
Settings, and Billing. Main content area on the right.

Dashboard content:
1. Welcome message with the user's first name and today's date
2. Stats row: 4 cards showing Total Projects, API Calls This Month,
   Storage Used, and Current Plan — fetch from getUserStats()
3. Recent Activity: a list of the last 10 actions (icon, description,
   timestamp in relative format like "2 hours ago")
4. Quick Actions: buttons for "New Project," "View Docs," "Upgrade Plan"

On mobile, the sidebar becomes a bottom navigation bar.
Use the existing auth context to get the current user.

You need to add a search feature to an existing blog that lets users find posts by title and content.

Pause and write your prompt before reading the suggestion.

Suggested prompt:

Add search functionality to the existing blog at app/blog/page.tsx.

Search bar:
- Place it at the top of the blog page, above the post grid
- As the user types, filter the displayed posts in real time (client-side)
- Debounce the input by 300ms to avoid excessive filtering
- Show the number of results: "Showing 5 of 24 posts"
- If no results, show "No posts found for '[query]'" with a
  "Clear search" button

Search should match against post title, description, and tags.
Highlight the matching text in the results using a yellow background.
Add the search query to the URL as a ?q= parameter so the search
is shareable and persists on refresh.
Keep the existing blog layout and card design unchanged.

Notice how each suggested prompt describes the user experience, specifies the location in the project, mentions existing patterns and functions, and handles edge cases. That's the standard you're aiming for.

What's Next

You now have a framework for writing prompts that produce excellent results. But writing great prompts for every individual request is just part of the equation. What if you could set up persistent instructions that automatically apply to every interaction?

In the next lesson, we'll dive into CLAUDE.md — the configuration file that gives your AI assistant permanent context about your project. It's like writing the onboarding document for a new developer who will work on your project every single day. Get it right, and every prompt you write becomes automatically better.