Edit on GitHub

Practical Workshop: Coding with Cursor

This section is the hands-on part of the workshop.
Participants build a tiny feature end-to-end and experience a safe, structured workflow with Cursor.

The example is intentionally small so we can focus on workflow and habits, not on framework details.


1. Setup

What you need

  • Node 18+ installed.
  • Cursor installed and logged in.
  • Git + a local clone of the workshop repo.

Get started

cd examples/express-todo-ts
pnpm install

Orientation

The project is a tiny in-memory Todo API built with Express + TypeScript:

examples/express-todo-ts/
  src/
    models/todo.ts           # Todo type definition
    routes/todos.ts          # Route handlers (GET, POST)
    services/todoService.ts  # In-memory store + helper functions
    app.ts                   # Express app setup
    server.ts                # Entry point
  tests/
    todos.test.ts            # Jest + supertest tests

Existing endpoints:

  • GET /todos – list all todos from the in-memory store.
  • POST /todos – create a todo with title and completed = false.

2. The Feature

We extend the existing Todo API with a new endpoint:

POST /todos/:id/complete – mark a todo as completed.

Scenario Response
Todo exists Set completed = true, return the updated todo (200 OK)
Todo does not exist Return 404
Already completed Return the current state unchanged (idempotent)

We build this using Cursor in a disciplined workflow:

  1. Understand the existing code.
  2. Write tests first.
  3. Generate implementation with AI.
  4. Review, run tests, and commit.

3. Step-by-Step Workshop Flow

Step 0 – Open in Cursor

  1. Open examples/express-todo-ts in Cursor.
  2. Point out the project structure: src/models, src/routes, src/services, tests/.
  3. Note that this project has no .cursorrules or AGENTS.md yet – the AI has zero project-specific context.

Talking point: “Notice how there are no rules files. The AI doesn’t know our conventions. Keep that in mind as you review what it generates.”


Step 1 – Explore with Ask mode

In Cursor chat (Ask mode), run:

We're in the Express + TypeScript project at @examples/express-todo-ts.

Question:
- Where is the Todo API implemented?

Please:
- List the main files (models, routes, services, tests).
- Summarize in 3-5 bullet points how it currently works.
- Do not change any code yet.

Open the files Cursor mentions:

  • src/models/todo.ts
  • src/routes/todos.ts
  • src/services/todoService.ts
  • tests/todos.test.ts

Teaching points:

  • Use @-mentions (@src, @tests) to give AI better context.
  • Verify what AI says by reading the files it references.
  • AI can summarize incorrectly – always check.

Step 2 – Write failing tests first

In chat (Plan or Agent mode):

We want to add a feature to the Express + TypeScript Todo API at @examples/express-todo-ts:

- Endpoint: POST /todos/:id/complete
- Behavior:
  - If the todo exists, set completed = true and return the updated todo (200 OK).
  - If the todo does not exist, return 404.
  - If the todo is already completed, just return the current state (idempotent).

Stack:
- Express 4
- TypeScript
- Existing tests under @examples/express-todo-ts/tests using Jest + supertest.

Tasks:
1. Propose 3-5 Jest test cases for this behavior.
2. Write the test functions into the appropriate test file, matching the existing style.
3. Stop after the tests -- do not implement the endpoint yet.

Walk participants through:

  • Where the tests were added (tests/todos.test.ts).
  • How the tests document the intended behavior of POST /todos/:id/complete.

Run pnpm test and show that the new tests fail. That’s the point – tests define the spec before any implementation exists.


Step 3 – Implement to make tests pass

Once tests are failing:

Now implement the feature in the Express + TypeScript project so that the new tests pass.

Context:
- Routes live in @examples/express-todo-ts/src/routes/todos.ts
- The in-memory store and helper functions live in @examples/express-todo-ts/src/services/todoService.ts

Constraints:
- Keep existing behavior of the Todo API unchanged.
- If you need to touch multiple files, list them first.
- Prefer small, focused changes.

You can demonstrate either:

  • Inline edit (Cmd/Ctrl+K) directly inside src/routes/todos.ts and src/services/todoService.ts.
  • Composer / Agent mode for a multi-file change with plan-review-execute discipline.

Review the proposed changes together:

  • Does it satisfy the tests?
  • Does it duplicate logic that already exists?
  • Any obvious security or performance issues?
  • Could you explain this in a code review?

Apply the edits and run pnpm test again. All tests should pass.


Step 4 – Review and commit

Review checklist:

  • All tests pass (old + new)
  • Implementation matches the spec
  • No unrelated changes crept in
  • No security issues (auth, validation, logging)
  • Could I explain this in a code review?

Commit small and descriptive:

git add tests/todos.test.ts
git commit -m "test: add tests for POST /todos/:id/complete"

git add src/
git commit -m "feat: implement todo completion endpoint"

4. Refactoring Exercise

After Step 3, review the AI-generated code. There’s a good chance the AI put the lookup-and-mutate logic directly in the route handler – something like:

todosRouter.post("/:id/complete", (req, res) => {
  const todo = listTodos().find((t) => t.id === id);
  todo.completed = true;
  return res.status(200).json(todo);
});

This works, but it violates separation of concerns. The route handler is doing work that belongs in the service layer.

The prompt

Ask participants to refactor using AI:

We want to improve the design of the Express + TypeScript Todo API at @examples/express-todo-ts:

Current situation:
- The POST /:id/complete route handler in src/routes/todos.ts does its own array
  lookup and mutation instead of calling a service function.
- Business logic for completing a todo lives in the route, not the service layer.

Goal:
- Keep route handlers thin.
- Add a completeTodo(id: string) function in src/services/todoService.ts.
- Update the route to call completeTodo() instead of doing the lookup inline.
- Keep behavior and tests unchanged.

Tasks:
1. Propose the new completeTodo() function.
2. Update the route to use it.
3. Verify tests still pass.

Discussion points

After the refactor, discuss:

  • Is the new structure easier to test in isolation?
  • Would a new developer find the behavior faster in the service layer or the route?
  • How hard would it be to swap the in-memory store for a real database now?
  • Which “golden rules” did this refactor reinforce? (Small steps, mandatory review, better context.)

5. Reflection & Discussion

After the feature works, debrief with the group:

  • What felt fast?
    Boilerplate, test scaffolding, repetitive patterns.
  • Where did human judgment matter most?
    Deciding the endpoint behavior and constraints. Reviewing the implementation and edge cases.
  • Which rules from the “Twelve Golden Rules” did we actually use?
    Context-first prompting. Iterative approach (design -> tests -> code). Mandatory code review.

Ask participants to name at least one place where AI would have gone wrong if nobody reviewed the code.


6. Optional Extensions (for longer workshops)

If you have more time, each extension reinforces the same core habits: think first, prompt with context, keep steps small, review everything.

Error handling & logging

  • Add structured logs to the completion endpoint.
  • Use AI to propagate consistent logging across all routes.

Security checks

  • Require authentication for the /complete endpoint.
  • Ask AI for potential security pitfalls in the new code.

Input validation

  • What happens if :id is not a valid format?
  • Add validation and appropriate error responses.

Database migration

  • Replace the in-memory store with SQLite or a similar lightweight database.
  • See how the service-layer abstraction (from the refactoring exercise) makes this easier.