Introduction to AIDD

(And How I Use Cursor to Be More Productive)

What are Developers Using AI For?

  • App Ideation/Planning
  • Bootstrapping new project code
  • Writing code for new features
  • Debugging 
  • Refactoring
  • Reviewing code in PRs
  • Writing unit and integration tests
  • Explaining existing code
  • Learning from real world code
  • Writing documentation
  • Migrating dependencies to new major versions
  • Converting logic from one language to another
  • Converting components from one framework to another
  • Write less human friendly code like regexes
  • Rapid prototyping
  • Quickly copying designs

How are developers interacting with AI?

The AI Assistance Spectrum

Browser Based AI Chats

Level 1

  • Easiest to get started with
  • Great for planning and research
  • Context limited to what you give it

The AI Assistance Spectrum

In Context AI Co-Pilot

Level 2

  • Chat, autocomplete, and targeted edits in programs like Cursor and Github Copilot
  • Smartly adds relevant context from an indexed codebase
  • Takes into account custom rules saved with the repo (and used for all team-members)
  • Developer retains high level of control

Browser Based AI Chats

Level 1

Inline Autocomplete

(click to enlarge)

Chat

(click to enlarge)

The AI Assistance Spectrum

Supervised Agentic Coding (Flow)

Level 3

  • AI is given goals to achieve and tools to execute those goals
  • Acts more as pair programmer than a mere development tool
  • AI is given more freedom, while human is still attentive to provide feedback, change direction, etc

In Context AI Co-Pilot

Level 2

Browser Based AI Chats

Level 1

The AI Assistance Spectrum

Supervised Agentic Coding (Flow)

Level 3

Autonomous Agents

Level 4

  • Given a goal, a well-laid plan, and tools
  • Tools MUST include feedback gathering
  • Runs without supervision
  • Can run multiple in parallel
  • Human in the loop only a the end for review and testing during PR.

Browser Based AI Chats

Level 1

In Context AI Co-Pilot

Level 2

The AI Assistance Spectrum

Autonomous Agents

Level 4

Supervised Agentic Coding (Flow)

Level 3

Browser Based AI Chats

Level 1

You should be dabbling/aiming to get here

the future is not far off

You should at least be here

in your daily workflows

Which level are you at?

In Context AI Co-Pilot

Level 2

The AI Assistance Spectrum

Autonomous Agents

Level 4

Supervised Agentic Coding (Flow)

Level 3

Browser Based AI Chats

Level 1

In Context AI Co-Pilot

Level 2

The AI Assistance Spectrum

Security and Control

Supervised Agentic Coding (Flow)

Level 3

Tools

  • Cursor
  • Github Co-Pilot
  • Jetbrains IDE w/ Junie
  • Windsurf (owned by OpenAI)
  • Cline
  • Augment
  • Roo Code (open source)

IDEs

*

**

*

**both plugin and stand alone IDE

* IDE plugin

  • Claude Code
  • Gemini CLI
  • OpenAI Codex

CLIs

**ALL are open source

* New and basically FREE!

*

  • Bolt
  • Lovable
  • Replit

Web-Based

Perfect for Vibe Coding

*

Tools

  • Cursor Background Agents
  • Github Co-pilot Background Agents
  • OpenAI Codex (not CLI)

** ALL Run in a secure cloud environment in dedicated git branches

Autonomous Agents

Level 4

Anybody curious to see how a background agent looks in cursor?

(I can demo)

Prompt Engineering and AI Agents

for Developers

ie Some Ways I make Cursor Work for Me

General Prompting

and then take a look at 

a more practical case study example

Listing out Some Simple Rules

we'll start by

Prompt Rule:

 Provide Examples

  • Zero-shot: No example
  • One-shot: One example
  • Few-shot: Several examples

Let’s see each in action →

Zero-Shot Prompting

⚠️ Technically wrong — it’s PascalCase, not camelCase

Prompt: Convert this string to camelCase: user_profile_image

AI Output: UserProfileImage

One-Shot Prompting

⚠️ Better than zero-shot, but not always consistent with edge cases

AI Output: userProfileImage

"user_id" → "userId"

Now convert: "user_profile_image"

User Prompt:

Few-Shot Prompting

✅ Correct and consistent output thanks to pattern examples

AI Output: userProfileImage

"user_id" → "userId"
"access_token" → "accessToken"
"api_response_body" → "apiResponseBody"

Now convert: "user_profile_image"

User Prompt:

Recap: Examples in Prompting

  • Zero-shot is fastest — give it a try!
  • One-shot tunes style with just one example.
  • Few-shot builds consistency through patterns.
  • You may not always need more than zero shot — experiment per use case!

Use XML For Grouping Concerns

When your prompt contains multiple types of input, structure them clearly using XML.

This helps the model distinguish between different roles or intents in your message.

Use tags like <code>, <goal>, <constraints>

Prompt Rule

Markdown Headings also good for grouping

## General Component Rules
- Rule 1
- Etc

## Component Definition Rules
- Rule 1
- Etc

## Component Usage Rules
- Rule 1
- Etc

Prompt Rule

Cursor Rules

These past 2 tips are even more useful in rules files that direct prompts.

Or whatever your flavor is...

Anybody want further explanation on Cursor Rules?

Don't let AI start from scratch every time

Prompt Rule

It's tempting I know.... I've fallen victim more times than I'd like to admit

Dont' start from scratch

Give the AI tools to generate new things from a template

Hygen

A JavaScript Friendly File Code Generator from Templates

  • Generates Code From Templates
  • Uses EJS for templating variables, conditionals, etc
  • Produces deterministic results
  • Can add comments inside template for AI to replace with non-deterministic content

Hygen

since agents can run commands, just make sure your cursor rules let them know about the available hygen commands

// cursor rule

* Whenever you're doing task A start by running the command: 
	`npx hygen thingFromTemplate new --name [singular-thing-name-here]`

Mention Files as Examples

User Prompt: add a calendar component 

AI Output: a (maybe) working component that follows none of your conventions 

Mention Files as Examples

User Prompt:  create a calendar component. Read 2-3 existing component files for inspiration especially @similarComponent

AI Output: a (more likely) working component that DOES follow more of your conventions 

Limit Your Request Scope

Don't ask for a full on app with all the bells and whistles. Go one step at a time.

WHY?

Because you have control issues?

No!

Because when you ask for a scoped and limited result - The AI does one or 2 things quite well

But if you ask for many things - the AI touches a lot of different aspects of the request but usually never completes any of them adequately

Limit Your Request Scope

For example:

  • First brainstorm to get the essential MVP features
  • Start with database tables
  • Then create API endpoints 
  • Next write tests for those API endpoints
  • Create a UI (components) that are ready to work with (but doesn't speak to API)
  • Write tests for the components
  • Hookup the API UI to the API

Use Case Example

Simple blog 

(This full process we'll walk through now can be found commit by commit in the aidd-posts-example branch

Brainstorming

My prompt

I skipped this step since a blog is a pretty common project 

btw, yes I know a blog is pretty simple but I've used the same or similar rules in other larger production contexts successfully

(NOTE: that I do primarily work on content-related and exercise projects, but do also freelance for a client on a Nuxt production app  )

Database

My prompt

Limiting scope to database concerns

Human guided specific context

* This project uses drizzle as a databse ORM. 
* Project Schema - @drizzle-schema.ts
* On the server side, call the auto-imported `useDb()` function
* Seeds should be created as nitro tasks:

```ts
// server/tasks/seed/postsSeed.ts
export default defineTask({
  meta: {
      name: "seed:posts",
      description: "Seed Posts",
    },
  run({ payload, context }) {
    const db = useDb();
    // do seeding
    return { result: "Success" };
  },
});
```

Database

My Prompt

The @database rules

One shot example

was enough

Database

The Result

I got an TS Error 

but it was easily fixed by hand after a couple failed attempts from the AI

Database

The Result

## Run this in terminal 👇

npx drizzle-kit push
npx drizzle-kit studio

DB Table setup 🎉

Database Summary

  • 2 Prompts
    1 to create and one to attempt TS fix
  • 1Manual Fix
  • About 10 Mins of work
  • Room for improvement:
    • Give LLM info about how to run seeds itself

* NOTE: if you look in the git history each commit takes a bit longer than reported here but I was also working on slides, workshop content, etc at the same time

API Endpoints

My Prompt

Very specific about what I want

remember my TS error, it's important to tell the LLM you manually made some changes, if it often reverts it back (within the same chat)

Included API specific rules 

API

My Rules File

The @api rules

* All API endpoints follow the OpenAPI specification.

* When creating new resources (like users, posts, etc) use the following command to bootsrap all CRUD API endpoints: `npx hygen resource new --name [singular-resource-name-here]`

* ALWAYS read the created files for more direction (feel free to delete any that end up not being used)

* When creating one-off API endpoints (not full resources) bootstrap the API file with the command: `npx hygen api new --path [path-relative to server/api] --method [get, post, put, delete]`

* ALWAYS read the created files after running the hygen command.

* If you need to create an API endpoint that supports streaming:
  *  Don't use one of the hygen commands
  *  DO use the Nuxt MCP for reference
  *  Do define the return response with the `sendStream` function











Ensured the AI knew what it was working with

Used templated api files

API

The Result

Because I used a template with placeholder content in it for openapi api documentation

+

I had Nitro OpenAPI support turned on

http://localhost:3000/_swagger

API

The Result

Also generates scalar style API docs

http://localhost:3000/_scalar

But it had an issue with my success response error properties

which turned out to be a bug/limitation in scalar's adherence to the OpenAPI Spec

API Summary

  • 1 Prompt
  • About 10 Mins of work
     

* NOTE: if you look in the git history each commit takes a bit longer than reported here but I was also working on slides, workshop content, etc at the same time

API Summary

Bonus callout!

Cursor decided on it's own to run curl requests to each of the different API endpoints to test

Claude 4 Sonnet

API Summary

💡 IDEA!

If working on an app with auth, engineer a way for AI to send auth headers to in-personate a logged in user OR provide a way for AI (in dev) to bypass auth as a custom user

API Tests

My Prompt

Notice that it picked up on my testing file automatically without me recommending...

API Tests

My Rules File

The @testing rules

* If ever in doubt, use context7 mcp or search the web to help write a unit test
* More tests documentation found in @README.md.(tests/README.md)

## Unit Tests
* Unit tests are written with Vitest
* Are written alongside the code they are testing (in the same directory)

## E2E Tests
* E2E tests are written with Playwright
* E2E tests are written in the tests/e2e/ directory





Give hints about when a certain mcp is helpful

Point the LLM to other relevant files

API Tests

The Result

// api-posts.spec.ts

/* eslint-disable @typescript-eslint/no-explicit-any */
import { test, expect } from "@nuxt/test-utils/playwright";

// Type definitions for API responses
interface Post {
  id: number;
  title: string;
  slug: string;
  content: string;
  description?: string;
  author: string;
  status: "draft" | "published";
  published_at: string | null;
  created_at: string;
  updated_at: string;
}

test.describe("/posts API endpoints", () => {
  let createdPostId: number;

  test.beforeEach(async ({ page }) => {
    // Set up common request headers
    await page.setExtraHTTPHeaders({
      "Content-Type": "application/json",
    });
  });

  test.describe("GET /api/posts", () => {
    test("should list all posts with default pagination", async ({ page }) => {
      const response = await page.request.get("/api/posts");
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(data.error).toBe(false);
      expect(data.statusMessage).toBe("Retrieved posts successfully");
      expect(Array.isArray(data.data)).toBe(true);
      expect(data.pagination).toMatchObject({
        page: 1,
        limit: 20,
        total: expect.any(Number),
      });
    });

    test("should filter posts by status", async ({ page }) => {
      const response = await page.request.get("/api/posts?status=published");
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(data.data.every((post: any) => post.status === "published")).toBe(
        true
      );
    });

    test("should filter posts by draft status", async ({ page }) => {
      const response = await page.request.get("/api/posts?status=draft");
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(data.data.every((post: any) => post.status === "draft")).toBe(
        true
      );
    });

    test("should search posts by title and content", async ({ page }) => {
      const response = await page.request.get("/api/posts?search=TypeScript");
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(data.data.length).toBeGreaterThan(0);
      expect(
        data.data.some(
          (post: any) =>
            post.title.includes("TypeScript") ||
            post.content.includes("TypeScript") ||
            post.description?.includes("TypeScript")
        )
      ).toBe(true);
    });

    test("should filter posts by author", async ({ page }) => {
      const response = await page.request.get("/api/posts?author=Alex");
      const data = await response.json();

      expect(response.status()).toBe(200);
      if (data.data.length > 0) {
        expect(
          data.data.every((post: any) => post.author.includes("Alex"))
        ).toBe(true);
      }
    });

    test("should paginate results correctly", async ({ page }) => {
      const response = await page.request.get("/api/posts?page=1&limit=2");
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(data.data.length).toBeLessThanOrEqual(2);
      expect(data.pagination.page).toBe(1);
      expect(data.pagination.limit).toBe(2);
    });

    test("should sort posts by creation date desc by default", async ({
      page,
    }) => {
      const response = await page.request.get("/api/posts?limit=5");
      const data = await response.json();

      expect(response.status()).toBe(200);
      if (data.data.length > 1) {
        const dates = data.data.map((post: any) =>
          new Date(post.created_at).getTime()
        );
        const sortedDates = [...dates].sort((a, b) => b - a);
        expect(dates).toEqual(sortedDates);
      }
    });

    test("should sort posts by title ascending", async ({ page }) => {
      const response = await page.request.get(
        "/api/posts?sortBy=title&sortOrder=asc&limit=5"
      );
      const data = await response.json();

      expect(response.status()).toBe(200);
      if (data.data.length > 1) {
        const titles = data.data.map((post: any) => post.title);
        const sortedTitles = [...titles].sort();
        expect(titles).toEqual(sortedTitles);
      }
    });

    test("should return empty results for non-existent search", async ({
      page,
    }) => {
      const response = await page.request.get(
        "/api/posts?search=NonExistentSearchTerm12345"
      );
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(data.data).toEqual([]);
      expect(data.pagination.total).toBe(0);
    });
  });

  test.describe("POST /api/posts", () => {
    test("should create a new post successfully", async ({ page }) => {
      const newPost = {
        title: "Test Post Creation",
        content: "This is a test post created via API testing.",
        description: "Test post description",
        author: "Test Author",
        status: "draft",
      };

      const response = await page.request.post("/api/posts", {
        data: newPost,
      });
      const data = await response.json();

      expect(response.status()).toBe(201);
      expect(data.error).toBe(false);
      expect(data.statusMessage).toBe("Post created successfully");
      expect(data.data).toMatchObject({
        id: expect.any(Number),
        title: newPost.title,
        slug: "test-post-creation",
        content: newPost.content,
        description: newPost.description,
        author: newPost.author,
        status: "draft",
        published_at: null,
        created_at: expect.any(String),
        updated_at: expect.any(String),
      });

      // Store the created post ID for cleanup
      createdPostId = data.data.id;
    });

    test("should auto-generate slug when not provided", async ({ page }) => {
      const newPost = {
        title: "My Amazing Blog Post Title!",
        content: "Content here",
        author: "Test Author",
      };

      const response = await page.request.post("/api/posts", {
        data: newPost,
      });
      const data = await response.json();

      expect(response.status()).toBe(201);
      expect(data.data.slug).toBe("my-amazing-blog-post-title");

      // Clean up
      await page.request.delete(`/api/posts/${data.data.id}`);
    });

    test("should create published post with published_at timestamp", async ({
      page,
    }) => {
      const newPost = {
        title: "Published Test Post",
        content: "This post should be published immediately",
        author: "Test Author",
        status: "published",
      };

      const response = await page.request.post("/api/posts", {
        data: newPost,
      });
      const data = await response.json();

      expect(response.status()).toBe(201);
      expect(data.data.status).toBe("published");
      expect(data.data.published_at).not.toBeNull();
      expect(new Date(data.data.published_at)).toBeInstanceOf(Date);

      // Clean up
      await page.request.delete(`/api/posts/${data.data.id}`);
    });

    test("should validate required fields", async ({ page }) => {
      const invalidPost = {
        description: "Missing required fields",
      };

      const response = await page.request.post("/api/posts", {
        data: invalidPost,
      });
      const data = await response.json();

      expect(response.status()).toBe(422);
      expect(data.error).toBe(true);
      expect(data.statusMessage).toBe("Invalid request payload");
      expect(data.data.issues).toEqual(
        expect.arrayContaining([
          expect.objectContaining({
            path: ["title"],
            message: expect.stringContaining("Required"),
          }),
          expect.objectContaining({
            path: ["content"],
            message: expect.stringContaining("Required"),
          }),
          expect.objectContaining({
            path: ["author"],
            message: expect.stringContaining("Required"),
          }),
        ])
      );
    });

    test("should validate field lengths", async ({ page }) => {
      const invalidPost = {
        title: "",
        content: "",
        author: "",
        description: "a".repeat(501), // Exceeds 500 char limit
      };

      const response = await page.request.post("/api/posts", {
        data: invalidPost,
      });
      const data = await response.json();

      expect(response.status()).toBe(422);
      expect(data.error).toBe(true);
      expect(data.data.issues).toEqual(
        expect.arrayContaining([
          expect.objectContaining({
            path: ["description"],
            message: expect.stringContaining("500"),
          }),
        ])
      );
    });
  });

  test.describe("GET /api/posts/[id]", () => {
    test("should retrieve a specific post by ID", async ({ page }) => {
      // First, get a valid post ID from the list
      const listResponse = await page.request.get("/api/posts?limit=1");
      const listData = await listResponse.json();
      const postId = listData.data[0].id;

      const response = await page.request.get(`/api/posts/${postId}`);
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(data.error).toBe(false);
      expect(data.statusMessage).toBe("Post retrieved successfully");
      expect(data.data).toMatchObject({
        id: postId,
        title: expect.any(String),
        slug: expect.any(String),
        content: expect.any(String),
        author: expect.any(String),
        status: expect.stringMatching(/^(draft|published)$/),
        created_at: expect.any(String),
        updated_at: expect.any(String),
      });
    });

    test("should return 404 for non-existent post", async ({ page }) => {
      const response = await page.request.get("/api/posts/999999");
      const data = await response.json();

      expect(response.status()).toBe(404);
      expect(data.error).toBe(true);
      expect(data.statusMessage).toBe("Post not found");
    });

    test("should validate ID parameter", async ({ page }) => {
      const response = await page.request.get("/api/posts/invalid-id");

      // Should return error due to invalid ID format
      expect(response.status()).not.toBe(200);
    });
  });

  test.describe("PUT /api/posts/[id]", () => {
    let testPostId: number;

    test.beforeEach(async ({ page }) => {
      // Create a test post for updates
      const createResponse = await page.request.post("/api/posts", {
        data: {
          title: "Post to Update",
          content: "Original content",
          author: "Original Author",
          status: "draft",
        },
      });
      const createData = await createResponse.json();
      testPostId = createData.data.id;
    });

    test.afterEach(async ({ page }) => {
      // Clean up test post
      if (testPostId) {
        await page.request.delete(`/api/posts/${testPostId}`);
      }
    });

    test("should update post fields successfully", async ({ page }) => {
      const updateData = {
        title: "Updated Post Title",
        description: "Updated description",
        status: "published",
      };

      const response = await page.request.put(`/api/posts/${testPostId}`, {
        data: updateData,
      });
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(data.error).toBe(false);
      expect(data.statusMessage).toBe("Post updated successfully");
      expect(data.data).toMatchObject({
        id: testPostId,
        title: updateData.title,
        slug: "updated-post-title",
        description: updateData.description,
        status: "published",
        published_at: expect.any(String), // Should be set when status changes to published
        updated_at: expect.any(String),
      });
    });

    test("should perform partial updates", async ({ page }) => {
      const updateData = {
        title: "Only Title Updated",
      };

      const response = await page.request.put(`/api/posts/${testPostId}`, {
        data: updateData,
      });
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(data.data.title).toBe(updateData.title);
      expect(data.data.content).toBe("Original content"); // Should remain unchanged
      expect(data.data.author).toBe("Original Author"); // Should remain unchanged
    });

    test("should auto-generate slug when title is updated", async ({
      page,
    }) => {
      const updateData = {
        title: "New Title With Special Characters!",
      };

      const response = await page.request.put(`/api/posts/${testPostId}`, {
        data: updateData,
      });
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(data.data.slug).toBe("new-title-with-special-characters");
    });

    test("should handle publishing via status update", async ({ page }) => {
      const updateData = {
        status: "published",
      };

      const response = await page.request.put(`/api/posts/${testPostId}`, {
        data: updateData,
      });
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(data.data.status).toBe("published");
      expect(data.data.published_at).not.toBeNull();
    });

    test("should handle unpublishing via status update", async ({ page }) => {
      // First publish the post
      await page.request.put(`/api/posts/${testPostId}`, {
        data: { status: "published" },
      });

      // Then unpublish it
      const response = await page.request.put(`/api/posts/${testPostId}`, {
        data: { status: "draft" },
      });
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(data.data.status).toBe("draft");
      expect(data.data.published_at).toBeNull();
    });

    test("should return 404 for non-existent post", async ({ page }) => {
      const response = await page.request.put("/api/posts/999999", {
        data: { title: "Updated Title" },
      });
      const data = await response.json();

      expect(response.status()).toBe(404);
      expect(data.error).toBe(true);
      expect(data.statusMessage).toBe("Post not found");
    });

    test("should validate updated field lengths", async ({ page }) => {
      const updateData = {
        title: "a".repeat(256), // Exceeds 255 char limit
        description: "b".repeat(501), // Exceeds 500 char limit
      };

      const response = await page.request.put(`/api/posts/${testPostId}`, {
        data: updateData,
      });
      const data = await response.json();

      expect(response.status()).toBe(422);
      expect(data.error).toBe(true);
    });
  });

  test.describe("DELETE /api/posts/[id]", () => {
    let testPostId: number;

    test.beforeEach(async ({ page }) => {
      // Create a test post for deletion
      const createResponse = await page.request.post("/api/posts", {
        data: {
          title: "Post to Delete",
          content: "This post will be deleted",
          author: "Test Author",
        },
      });
      const createData = await createResponse.json();
      testPostId = createData.data.id;
    });

    test("should delete post successfully", async ({ page }) => {
      const response = await page.request.delete(`/api/posts/${testPostId}`);
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(data.error).toBe(false);
      expect(data.statusMessage).toBe("Post deleted successfully");
      expect(data.data).toMatchObject({
        id: testPostId,
        deleted: true,
      });

      // Verify post is actually deleted
      const getResponse = await page.request.get(`/api/posts/${testPostId}`);
      expect(getResponse.status()).toBe(404);
    });

    test("should return 404 for non-existent post", async ({ page }) => {
      const response = await page.request.delete("/api/posts/999999");
      const data = await response.json();

      expect(response.status()).toBe(404);
      expect(data.error).toBe(true);
      expect(data.statusMessage).toBe("Post not found");
    });

    test("should return 404 for already deleted post", async ({ page }) => {
      // Delete the post
      await page.request.delete(`/api/posts/${testPostId}`);

      // Try to delete it again
      const response = await page.request.delete(`/api/posts/${testPostId}`);
      const data = await response.json();

      expect(response.status()).toBe(404);
      expect(data.error).toBe(true);
      expect(data.statusMessage).toBe("Post not found");
    });
  });

  test.describe("API Response Format", () => {
    test("should return consistent response format for success cases", async ({
      page,
    }) => {
      const response = await page.request.get("/api/posts?limit=1");
      const data = await response.json();

      expect(data).toMatchObject({
        error: false,
        url: expect.any(String),
        statusMessage: expect.any(String),
        statusCode: 200,
        message: expect.any(String),
        data: expect.any(Array),
        pagination: expect.any(Object),
      });
    });

    test("should return consistent error format", async ({ page }) => {
      const response = await page.request.get("/api/posts/999999");
      const data = await response.json();

      expect(data).toMatchObject({
        error: true,
        url: expect.any(String),
        statusCode: 404,
        statusMessage: expect.any(String),
        message: expect.any(String),
      });
    });
  });

  test.describe("Edge Cases", () => {
    test("should handle empty content gracefully", async ({ page }) => {
      const response = await page.request.get("/api/posts?search=");
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(Array.isArray(data.data)).toBe(true);
    });

    test("should handle large page numbers", async ({ page }) => {
      const response = await page.request.get("/api/posts?page=1000&limit=10");
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(data.data).toEqual([]);
      expect(data.pagination.page).toBe(1000);
    });

    test("should handle limit boundary values", async ({ page }) => {
      const response = await page.request.get("/api/posts?limit=1");
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(data.data.length).toBeLessThanOrEqual(1);
      expect(data.pagination.limit).toBe(1);
    });

    test("should handle maximum limit", async ({ page }) => {
      const response = await page.request.get("/api/posts?limit=100");
      const data = await response.json();

      expect(response.status()).toBe(200);
      expect(data.pagination.limit).toBe(100);
    });
  });

  test.afterAll(async ({ page }) => {
    // Clean up any remaining test posts
    if (createdPostId) {
      await page.request.delete(`/api/posts/${createdPostId}`);
    }
  });
});








It wrote unit tests that accurately tested many of the endpoints

let's look through a few tests

it created it in the exact right place, because my api hygen template run by the previous prompt included an api test boilerplate file

API Tests

The Result

But I know you're dying to know... how many passed?

29/33

4 failed

API Tests

The Result

the tests run by playwright

npm run test:e2e

(ran pretty slowly TBH but could be swapped out for other solutions)

feel free to check the failed once to debug if you'd like 

API Tests Summary

  • 1 Prompt
  • About 30 Mins of work

     
  • Room for improvement:
    Find different runner for API tests or different Playwright setup

* NOTE: if you look in the git history each commit takes a bit longer than reported here but I was also working on slides, workshop content, etc at the same time

I tried too long to fix the broken tests by getting the test runner to reseed the database before start and the test runner was slow!

Went to lunch on this one too! :) 

UI

My Prompt

My Prompt

FULL DISCLOSURE: I did a couple similar prompts just before this that errored out. To fix it took me a little time to:

  1. make an adjustment to component rules (I had made a big mistake when copying/pasting rules from a couple different other projects) 
  2. restored to before that message 
  3. tried with a tighter/ more specific prompt

UI

BTW, if anyone has a good strategy for easily sharing prompts between different projects and keeping them all up to date I would love to hear it :) 

UI

My Rules Files

The @components rules


## General Component Rules

- Shad-cn Vue is installed and all of it's components are stored in `/components/ui`. They can be for most common UI elements.

## Component Organization and Naming Conventions

- Organize components by feature within `app/components` for example:

```
app/components/
    User
        UserProfile // very important it has prefix of parent directory name (ie User)
            UserProfile.vue  // 👈 the "main" component
            UserProfileImage.vue   // used in UserProfile.vue
            UserProfileEdit.vue   // very important it has full prefix of parent directory name (ie UserProfile)
            UserProfile.nuxt.test.ts // 👈 test for the "main" component
    Post
        PostList
            PostList
            PostListHeader
            PostListRow
        PostCard
            PostCard
            PostCardImage
            PostCardFooter
```

- Organize base/general that are NOT ShadCN components within `app/components/Base`
- You should NOT structure custom project components directory or naming structure based on any ShadCN component

## Component Definition Rules

- Only create a new custom component is shad-cn doesn't already have a solution!! Rely primarily on ShadCN components!
- NEVER try to generate a ShadCN component, all supported components are already installed (you can scan the `app/components/ui` directory to see all available)
- Do NOT create long components with a lot of responsibilities. Instead, break components down into smaller, more focused components
- Create small, focused components (< 50 lines)
- break complex logic into composables or utility functions
- Always use TypeScript to define all components.
- Use Typescript to define component props and emits.
- use JS docs to document component props and emits
- Use script setup
- Provide the component sections in the following order:
  - script
  - template
  - style 

## Component Usage Rules

- Do NOT import components defined within the project source code. They are auto-imported based on the compnent name. For example:
  - `Post/PostList/PostList.vue` is used via <PostList>
  - `Post/PostList/PostListItem.vue` is used via <PostListItem>
  - `Post/PostCard/PostCardImage` is used via <PostCardImage>
- The naming convention usage for ShadCN components are an exception. They are always autoimported and prefixed with `Ui` for example <UiButton>, <UiTexarea>, etc


## Template Ref Rules

- When creating template refs that reference a native DOM element always name them with the `El` suffix. For example:
  -  `const textareaEl = useTemplateRef('textareaEl')`  
- When creating template refs that reference a Vue Component always name them with the `ComponentRef` suffix. For example: 
  - `const myComponentRef = useTemplateRef('myComponentRef')`   
- Always use `useTemplateRef('[ref identifier here]')` to define template refs (not `ref()`)

## Icons

- Nuxt Icon is installed. 
- Display an icon with the icon component. For example:
  - `<Icon name="lucide:refresh">
- Rember to prefix the icon with the packname (like `lucide:`)  
- Any iconify icon can be used

# Style

- Primarily use shadcn classes for color styles. For example:
  - bg-primary
  - text-primary-foreground
  - bg-muted
  - text-muted-foreground

- Pair bg and text-foreground colors appropriately. For example:
  - bg-primary and text-primary-foreground
  - bg-muted and text-muted-foreground

## Data Fetching 
* Prefer to fetch data at the page level most of the time and keep components free from network requests as it makes them easier to test. 





UI

My Rules Files

The @pages rules

* Prefer to fetch data at the page level most of the time. Ask me if you think you should do differently, based on the use case.
* When possible, rely on the type safe return (typed by Nuxt) of useFetch and don't manually type the response. For example:
  * DO THIS: `const {data} = await useFetch('/api/posts') // data is automatically typed`
  * NOT THIS: `const {data} = await useFetch<Post>('/api/posts') // manually typing response is bad and leads to maintainence issues`

UI

The Result

The @pages rules

* Prefer to fetch data at the page level most of the time. Ask me if you think you should do differently, based on the use case.
* When possible, rely on the type safe return (typed by Nuxt) of useFetch and don't manually type the response. For example:
  * DO THIS: `const {data} = await useFetch('/api/posts') // data is automatically typed`
  * NOT THIS: `const {data} = await useFetch<Post>('/api/posts') // manually typing response is bad and leads to maintainence issues`

UI

The Result

It got a little carried away and update the home paged too

No problem. I just removed that with git

UI

The Result

It created a nice looking and searchable post list (with ShadCN components!)

The published filter was right, and it went back to the server 

The pagination also worked but no buttons to move to between pages

UI

The Result

And a nice looking single post page

though it's content isn't parsed

No worries...

UI

The Result

And a nice looking single post page

though it's content isn't parsed

No worries...

UI

The Result

Now we have markdown support! 🎉

UI Summary

* NOTE: if you look in the git history each commit takes a bit longer than reported here but I was also working on slides, workshop content, etc at the same time

  • 3 - 4 Prompts
  • About 40 Mins of work
     

That copy/paste issue of component rules

  • 1 Prompt
  • About 5 Mins of work

After fixing the components prompts

(that should work for most components moving foward)

Miscellaneous Prompting Tips I've Heard or Experienced 

Ask If Familiar

Before asking AI to develop with your favorite solution ask if it's familiar with it

(can help you know if you should stick with technology x or find one the LLM is more familiar with)

Checklists for Complex Tasks

But now cursor has agent todos built-in

(as of just last week)

Add Links to Github READMEs

or better yet, use Context7

Use Ni to avoid mixups about package manager

Don't start from scratch:

  • use existing component libraries
  • use API standards like OpenAPI

Consider Asking Agents to use TDD

Create a new x component. Use TDD to develop it. Ensure you account for edge cases

Use TypeScript and ESLint As Much as Possible

Why?

Use TypeScript and ESLint As Much as Possible

Why?

These tools bring relevant context to agents at just the right time

So learn TypeScript if you don't know TypeScript

Install ESLint and Create Your Own Custom Rules for your Specific Project Patterns

When Prompting for UIs

  • use screenshots to show what you want or what an error is doing
  • use the playwright MCP to give agents insight into the browser console logs and UI rendering
  • Have a component playgrounds to point the model to good examples (can be storybook, or just custom pages)
  •  Use placeholder images from for easily visualizations:
    • https://picsum.photos
    • https://pravatar.cc

Optimizing Agent Workflows

use git work trees to enable working on multiple tasks at once

ALWAYS have this on!!

(Cursor Settings > Chat)

If you enjoyed this talk or this event, we'd love to hear from you on                           

trustpilot.com/review/aidd.io

Thank you for joining!

Hope to see you at a future event!

AIDD Day Talk

By Daniel Kelly

AIDD Day Talk

  • 171