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:
- requirements.md - What the feature needs to do
- design.md - How it will work
- 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
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`
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
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
Each document tells Kiro:
- What patterns to follow (e.g., "Use hexagonal architecture")
-
What to avoid (e.g., "Never use
anytypes") - 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
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:
- Kiro writes the test first (it fails - red)
- Kiro writes code to pass the test (it passes - green)
- 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);
});
});
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;
};
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");
});
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
anytypes)
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)
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
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:
- Port interface in
packages/core/src/ports/fit-reader.ts - Adapter implementation in
packages/core/src/adapters/fit/garmin-fitsdk.ts - Use case in
packages/core/src/application/use-cases/convert-fit-to-krd.ts - Complete test suite with 90%+ coverage
- Round-trip validation to ensure no data loss
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
]);
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);
}
};
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:
- Reads my spec from
.kiro/specs/[feature-name]/ - Gets the requirements, design, and tasks
- Generates a PR title from the main goal
- Fills the PR template with spec content
- Creates the PR using GitHub MCP (Model Context Protocol)
- 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
anytypes - 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
-
30+ specifications in
.kiro/specs/ -
18 steering documents in
.kiro/steering/ -
8 agent hooks in
.kiro/hooks/ - 100% of code generated with Kiro's help
Production Ready
-
3 npm packages published:
- @kaiord/core - Core library
- @kaiord/cli - Command-line tool
- 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:
- Source code: github.com/pablo-albaladejo/kaiord
- Live demo: pablo-albaladejo.github.io/kaiord
-
Specifications: Browse
.kiro/specs/to see real examples -
Steering docs: Read
.kiro/steering/for architecture rules
Key Files to Explore
Specifications (see how Kiro plans):
-
.kiro/specs/core/fit-to-krd-conversion/requirements.md- How Kiro planned FIT conversion -
.kiro/specs/core/zod-schema-migration/design.md- Schema-first approach
Steering Docs (see the rules Kiro follows):
-
.kiro/steering/architecture.md- Hexagonal architecture rules -
.kiro/steering/tdd.md- Test-driven development workflow -
.kiro/steering/zod-patterns.md- Schema-first patterns
Implementation (see the code Kiro generated):
-
packages/core/src/domain/schemas/duration.ts- Discriminated unions with Zod -
packages/core/src/application/use-cases/convert-fit-to-krd.ts- Clean use case -
packages/core/src/ports/fit-reader.ts- Port interface -
packages/core/src/adapters/fit/garmin-fitsdk.ts- Adapter implementation
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
- Kaiord Repository: github.com/pablo-albaladejo/kaiord
- Live Web App: pablo-albaladejo.github.io/kaiord
- npm Packages:
- Kiro IDE: kiro.dev
- Kiroween Hackathon: kiroween.devpost.com
Top comments (0)