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:
- make an adjustment to component rules (I had made a big mistake when copying/pasting rules from a couple different other projects)
- restored to before that message
- 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