DEV Community

Pablo Albaladejo for kirodotdev

Posted on

Stop Chatting, Start Specifying: Spec-Driven Design with Kiro IDE

Building Kaiord with Kiro: From Specs to Production

How Spec-Driven Development and Test-Driven Development Changed the Way I Code


The Problem with Chat-First Development

We've all been there. You open your AI assistant and start chatting:

"Can you help me convert FIT files to JSON?"

The AI immediately writes code. But wait, that's not exactly what you wanted. So you clarify:

"Actually, I need it to preserve all the workout data..."

More code appears. Still not quite right. Another message:

"No, I meant it should also handle repetition blocks..."

This back-and-forth conversation fills up the context window. By the time you get working code, there's no space left for the AI to help with the details. The problem: we're asking AI to write code before understanding what we need.


Enter Spec-Driven Development

When I discovered Kiro and its spec-driven approach, everything changed. Instead of jumping straight to code, I learned to think first, code later.

Here's how it works:

Step 1: Create a Specification with Kiro

Before writing any code, Kiro helps me create three documents by asking me questions:

  1. requirements.md - What the feature needs to do
  2. design.md - How it will work
  3. tasks.md - Step-by-step implementation plan

I tell Kiro "I need to convert FIT files to KRD format," and Kiro starts asking questions:

  • "What data needs to be preserved?"
  • "What are the acceptance criteria?"
  • "What tolerances are acceptable for round-trip conversion?"

Based on my answers, Kiro generates the specification documents. Let me show you a real example from Kaiord. When I needed to convert FIT files (Garmin's binary format) to KRD (my JSON format), Kiro created this spec:

# fit-to-krd-conversion/requirements.md

## User Story
As a cyclist or runner, I want to convert FIT workout files to KRD format, 
so that I can edit workout structures in a human-readable format and validate 
them against a standard schema.

## Acceptance Criteria
- WHEN the converter receives a valid FIT file
- THEN it SHALL produce a valid KRD document
- AND it SHALL preserve all workout steps
- AND it SHALL validate against the KRD schema
Enter fullscreen mode Exit fullscreen mode

You can see the full spec in .kiro/specs/core/fit-to-krd-conversion/requirements.md.

Why this matters: The spec becomes Kiro's guide. Instead of guessing what I want, Kiro has a clear document that captures my requirements through our conversation.

Step 2: Kiro Defines the Architecture

After understanding my requirements, Kiro proposes the architecture in the design document. It asks me questions like:

  • "Should we use hexagonal architecture with ports and adapters?"
  • "What external libraries will we use?"
  • "What are the key design goals?"

Based on my answers, Kiro generates the design document describing how the conversion will work:

# design.md

## Architecture

We'll create:
- Port interface: FitReader (domain/ports)
- Adapter implementation: garmin-fitsdk (adapters/fit)
- Use case: convertFitToKrd (application/use-cases)

### Key Design Goals

1. **Round-trip safety**: FIT → KRD → FIT preserves data within tolerances
2. **Hexagonal architecture**: Domain logic isolated from external dependencies
3. **Dependency injection**: Swappable FIT SDK implementations via ports
4. **Schema validation**: All KRD output validated against JSON schema
5. **Type safety**: Strict TypeScript with no implicit `any`
Enter fullscreen mode Exit fullscreen mode

This planning phase saved me hours of refactoring later. The code structure was clear before Kiro wrote a single line.

See .kiro/steering/architecture.md for the full architecture rules.

Step 3: Kiro Breaks Down Tasks

Finally, Kiro breaks the work into small, testable pieces in the tasks document:

# tasks.md

- [ ] Define FitReader port interface
- [ ] Implement Garmin FIT SDK adapter
- [ ] Create convertFitToKrd use case
- [ ] Write unit tests (AAA pattern)
- [ ] Add round-trip validation
- [ ] Document API examples
Enter fullscreen mode Exit fullscreen mode

Each task is small enough for Kiro to implement in one sitting, making progress visible and momentum sustainable. I review each task, and Kiro implements it following TDD.


Steering Documents: Teaching Kiro My Rules

Before Kiro writes any code, I need to teach it how I want things done. This is where steering documents come in.

Steering documents are markdown files in .kiro/steering/ that define the rules Kiro must follow. Think of them as a style guide, architecture manual, and best practices document all in one—but for AI.

Here's what I defined for Kaiord:

My Steering Documents

.kiro/steering/
├── architecture.md       # Hexagonal architecture rules
├── tdd.md               # Test-driven development workflow
├── code-style.md        # Code quality standards
├── zod-patterns.md      # Schema-first patterns
├── testing.md           # Testing strategies
├── error-patterns.md    # Error handling patterns
└── ... 18 documents total
Enter fullscreen mode Exit fullscreen mode

Each document tells Kiro:

  • What patterns to follow (e.g., "Use hexagonal architecture")
  • What to avoid (e.g., "Never use any types")
  • How to structure code (e.g., "Files ≤ 100 lines, functions < 40 LOC")
  • When to apply rules (e.g., "Write tests first, then implementation")

The key insight: Kiro reads these documents automatically while working. I don't need to repeat myself in every conversation. The rules are always there, guiding every decision Kiro makes.

Example: Architecture Steering Document

Here's a snippet from my .kiro/steering/architecture.md:

# Architecture — Hexagonal & DI

Layers:

- **domain/** — pure KRD types & rules
- **application/** — use-cases; depends on `ports/` only
- **ports/** — I/O contracts (Fit/Tcx/Zwift Reader/Writer)
- **adapters/** — concrete implementations (e.g., @garmin/fitsdk)
- **cli/** — end-user commands; depends on `application`

Rules:

- `domain` depends on no one
- `application` MUST NOT import external libs nor `adapters/`
- `adapters` implement `ports` and may use external libs
Enter fullscreen mode Exit fullscreen mode

When Kiro generates code, it automatically follows these architecture rules. If I accidentally try to import an adapter in a use case, Kiro warns me about the violation. I never have to remind Kiro about architectural boundaries—it's all in the steering document.


Growing Together: Managing Codebase Complexity

As a codebase grows, it's easy for things to get messy—especially when an AI can generate hundreds of lines in seconds. To keep our collaboration smooth and productive, I found that Kiro works best when we have clear processes and healthy boundaries.

Think of it as setting up guardrails so we can run fast without falling off a cliff.

1. The Safety Net: Test-Driven Development (TDD)

For me, TDD isn't just a methodology; it's how I communicate my intent to Kiro. It acts as our safety net, allowing us to build features and refactor code with confidence.

With TDD rules defined in my steering documents, Kiro follows this workflow automatically:

  1. Kiro writes the test first (it fails - red)
  2. Kiro writes code to pass the test (it passes - green)
  3. We refactor together (keep tests green - refactor)

Here's a real example from packages/core/src/application/use-cases/convert-fit-to-krd.ts:

Step 1: Kiro Writes the Test (Red)

// packages/core/src/application/use-cases/convert-fit-to-krd.test.ts
describe("convertFitToKrd", () => {
  it("should convert FIT workout to KRD format", async () => {
    // Arrange
    const fitBuffer = loadFitFixture("WorkoutIndividualSteps.fit");
    const fitReader = createMockFitReader();
    const validator = createSchemaValidator();

    // Act
    const krd = await convertFitToKrd(fitReader, validator)({
      fitBuffer,
    });

    // Assert
    expect(krd.version).toBe("1.0");
    expect(krd.type).toBe("workout");
    expect(krd.extensions.workout.steps).toHaveLength(4);
  });
});
Enter fullscreen mode Exit fullscreen mode

This test fails at first because convertFitToKrd doesn't exist yet.

Step 2: Kiro Writes Minimal Code (Green)

Only after writing the test does Kiro implement the actual conversion function:

// packages/core/src/application/use-cases/convert-fit-to-krd.ts
export const convertFitToKrd =
  (fitReader: FitReader, validator: SchemaValidator) =>
  async (params: ConvertFitToKrdParams): Promise<KRD> => {
    // Read FIT file
    const krd = await fitReader(params.fitBuffer);

    // Validate result
    const errors = validator.validate(krd);
    if (errors.length > 0) {
      throw new KrdValidationError("Validation failed", errors);
    }

    // Return validated KRD
    return krd;
  };
Enter fullscreen mode Exit fullscreen mode

The test passes! But we're not done yet.

Step 3: We Refactor Together

Now Kiro and I improve the code while keeping tests green. Kiro might suggest:

  • Extracting helper functions
  • Improving error messages
  • Adding edge case handling

I review the suggestions, and Kiro applies the changes—all while the tests continue to pass.

The AAA Pattern

All our tests follow the AAA pattern (Arrange-Act-Assert):

it("should validate KRD against schema", () => {
  // Arrange
  const validator = createSchemaValidator(mockLogger);
  const invalidKrd = { version: "1.0" }; // missing required fields

  // Act
  const errors = validator.validate(invalidKrd);

  // Assert
  expect(errors).toHaveLength(2);
  expect(errors[0].field).toBe("type");
  expect(errors[1].field).toBe("metadata");
});
Enter fullscreen mode Exit fullscreen mode

This pattern makes tests easy to read and understand. The Arrange section sets up test data, the Act section calls the function, and the Assert section checks the result.

The result: The code works the first time. No debugging sessions trying to figure out why something breaks.

You can see the TDD guidelines I defined in .kiro/steering/tdd.md. Kiro follows these rules for every function in Kaiord.

2. Healthy Boundaries: Small Files, Happy Code

Just like us, Kiro does better work when tasks are bite-sized. I've set up some helpful constraints that encourage us to keep things simple:

  • Keep files small (≤ 100 lines)
  • Keep functions focused (< 40 LOC)
  • Be specific (NO any types)

These aren't just rigid rules; they actually help Kiro write better, more maintainable code. When a file starts getting too long, Kiro notices and suggests, "Hey, maybe we should split this up?"

Example: When we were building the FIT converter, Kiro naturally organized the code into small, focused modules instead of one giant file:

adapters/fit/
├── garmin-fitsdk.ts          # Main entry (< 100 lines)
├── duration.mapper.ts         # Duration mapping (< 100 lines)
├── target.converter.ts        # Target conversion (< 100 lines)
└── workout.mapper.ts          # Workout mapping (< 100 lines)
Enter fullscreen mode Exit fullscreen mode

Each file stays under 100 lines, making the code easy to understand for both of us.

These automated checks run in the background, gently nudging us back to the right path if we stray. The result: A codebase that feels clean and manageable, no matter how much feature work we do.


Architecture Enforcement

I defined hexagonal architecture rules in steering docs that Kiro enforces. The key rule:

Domain depends on nothing. Application depends only on domain and ports. Adapters implement ports.

When Kiro generates code, it automatically:

  • Creates port interfaces (contracts)
  • Implements adapters (external libraries)
  • Writes use cases (business logic)
  • Wires everything in providers (dependency injection)

If I accidentally try to import an adapter in a use case, Kiro warns me:

⚠️ Architecture violation detected
   Application layer should not import adapters
   Import the port interface instead
Enter fullscreen mode Exit fullscreen mode

This keeps the codebase maintainable and testable. See .kiro/steering/architecture.md for the full rules I defined.


Real Examples from Kaiord

Let me show you three real examples where spec-driven development with Kiro made a huge difference:

Example 1: FIT to KRD Conversion

The challenge: Convert Garmin's binary FIT format to my JSON format without losing data.

The spec: .kiro/specs/core/fit-to-krd-conversion/requirements.md

What Kiro generated:

Time saved: What would have taken me 2 weeks of trial-and-error took 3 days with clear specifications.

Example 2: Duration Types with Discriminated Unions

The challenge: Support 14 different duration types (time, distance, calories, heart rate conditions, power conditions, etc.)

The spec: .kiro/specs/core/zod-schema-migration/design.md

What Kiro generated:

// From https://github.com/pablo-albaladejo/kaiord/blob/main/packages/core/src/domain/schemas/duration.ts
export const durationSchema = z.discriminatedUnion("type", [
  // Simple durations
  z.object({ type: z.literal("time"), seconds: z.number() }),
  z.object({ type: z.literal("distance"), meters: z.number() }),
  z.object({ type: z.literal("calories"), calories: z.number() }),

  // Conditional durations
  z.object({
    type: z.literal("heart_rate_less_than"),
    heartRate: z.number(),
    seconds: z.number().optional()
  }),
  z.object({
    type: z.literal("power_greater_than"),
    power: z.number(),
    seconds: z.number().optional()
  }),

  // ... 14 types total
]);
Enter fullscreen mode Exit fullscreen mode

The benefit: TypeScript knows exactly which fields exist for each duration type. No more runtime errors from accessing wrong fields!

Example 3: Round-Trip Validation

The challenge: Ensure conversions preserve data (FIT → KRD → FIT should match original).

The implementation:

// packages/core/src/application/use-cases/validate-round-trip.ts
export const validateRoundTrip = (
  fitReader: FitReader,
  fitWriter: FitWriter,
  checker: ToleranceChecker,
  logger: Logger
) => async (params: ValidateRoundTripParams): Promise<void> => {
  // Convert FIT → KRD → FIT
  const krd = await fitReader(params.fitBuffer);
  const convertedFit = await fitWriter(krd);

  // Check tolerances
  const deviations = checker.checkTolerances(
    params.fitBuffer,
    convertedFit
  );

  if (deviations.length > 0) {
    throw new ToleranceExceededError("Tolerances exceeded", deviations);
  }
};
Enter fullscreen mode Exit fullscreen mode

Tolerances:

  • Time: ±1 second
  • Power: ±1 watt or ±1% FTP
  • Heart Rate: ±1 bpm
  • Cadence: ±1 rpm

This ensures data integrity across all conversions.


Automation with Agent Hooks

One of Kiro's most powerful features is agent hooks - automated workflows that trigger based on events.

I created a hook that automatically generates GitHub pull requests from my specs. Here's how it works:

Hook configuration: .kiro/hooks/create-github-pr.kiro.hook

When I finish a feature, the hook:

  1. Reads my spec from .kiro/specs/[feature-name]/
  2. Gets the requirements, design, and tasks
  3. Generates a PR title from the main goal
  4. Fills the PR template with spec content
  5. Creates the PR using GitHub MCP (Model Context Protocol)
  6. Adds appropriate labels

Time saved: Creating a PR went from 10 minutes to 30 seconds.

Bonus: Every PR has complete documentation because it's generated from the spec!


The Numbers Tell the Story

After building Kaiord with Kiro's spec-driven approach:

Code Quality

  • 86.5% test coverage - More than most production apps
  • 300+ unit tests - TDD from day one
  • 55+ E2E tests - Playwright for integration testing
  • 0 any types - Complete type safety
  • 550+ TypeScript files - All following architecture rules

Test Coverage by Package

  • Core Package: 88% (target: ≥80%)
  • Converters/Mappers: 94% (target: ≥90%)
  • CLI Package: 82% (target: ≥70%)
  • Web SPA: 86.5% (target: ≥70%)

Kiro Integration

Production Ready

  • 3 npm packages published:
  • 1 web app deployed: Live demo
  • 5 CI/CD workflows - Automated testing, linting, deployment

What I Learned

1. Planning Saves Time

Writing specs feels slow at first. But it's much faster than:

  • Rewriting code because requirements changed
  • Debugging code you don't understand
  • Explaining to the AI what you already explained 5 times

Example: The FIT conversion spec took 2 hours to write. It saved me 2 weeks of confusion.

2. Tests Give Confidence

Following TDD means I know my code works. When I add features, the tests tell me immediately if I broke something.

Example: When I added support for swimming workouts, my existing tests caught 3 bugs before I even ran the app.

3. Architecture Matters

Using hexagonal architecture (ports and adapters) made my code:

  • Easy to test (mock the ports)
  • Easy to extend (add new adapters)
  • Easy to understand (clear boundaries)

Example: Adding TCX format support required zero changes to existing code - just a new adapter.

4. AI Works Better with Structure

Kiro's spec-driven approach gives AI the context it needs to help effectively. Instead of guessing, it implements what's documented.

5. Specs Save Time

Clear specs mean:

  • Less confusion during implementation
  • Fewer bugs from misunderstood requirements
  • Better code reviews
  • Easier onboarding for new developers

6. TDD Prevents Bugs

Writing tests first catches bugs early. When we write a test, we think about:

  • What should happen in normal cases?
  • What should happen in edge cases?
  • What should happen when errors occur?

This thinking prevents many bugs before they're written.


Try It Yourself

Want to see spec-driven development in action? Check out Kaiord:

Key Files to Explore

Specifications (see how Kiro plans):

Steering Docs (see the rules Kiro follows):

Implementation (see the code Kiro generated):


Final Thoughts

Building Kaiord taught me that AI doesn't replace thinking - it amplifies it.

With Kiro's spec-driven approach:

  • I think more clearly about what I need
  • I document decisions before coding
  • I test continuously (TDD)
  • I maintain clean architecture
  • AI implements what I specify

The result? Production-ready code that:

  • Works reliably (86.5% test coverage)
  • Makes sense (clean architecture)
  • Can grow (extensible design)
  • Helps others (open source)

Spec-driven development isn't slower - it's smarter. The time you spend planning pays back 10x in code that works the first time.


Resources


Top comments (0)