Spec-driven development is an approach where a detailed specification is the single source of truth for building software. This practice ensures that business requirements, tests, and application code all align, minimizing the drift that often occurs when these components are developed in isolation.
Traditionally, development might start with a rough idea, followed by code, with tests written afterward (if at all). Methodologies like Test-Driven Development (TDD) improved this by putting tests first. Spec-driven development is the next logical evolution, especially in the age of AI. By starting with a clear, machine-readable (or at least machine-understandable) specification, we can automate the generation of both the tests that validate the feature and the code that implements it.
AI acts as a powerful accelerator in the spec-driven workflow, creating a tight, automated loop between the specification, the tests, and the code. This process ensures that what you specified is what you tested, and what you tested is what you built.
The AI-powered Spec → Test → Code
pipeline works like this:
- Write a Clear Specification: The process starts with a human writing a detailed description of the feature. This could be a user story with acceptance criteria, a Gherkin file for Behavior-Driven Development (BDD), or even a well-commented function signature.
- AI Generates Tests: An AI model consumes the specification and generates the corresponding unit, integration, or API tests. Because the AI has the full context of the requirements, it can create test cases that cover expected outcomes, edge cases, and error handling.
- AI Generates Code: Using the exact same specification, the AI model generates the application code designed to fulfill the requirements and pass the previously generated tests.
- CI/CD Pipeline Executes: A continuous integration (CI) pipeline automatically runs the generated tests against the generated code. If all tests pass, the loop is closed, and you have high confidence that the code perfectly matches the specification.
This automated cycle dramatically reduces the manual effort of writing boilerplate code and tests, freeing up developers to focus on complex logic and architecture.
Let’s walk through a simplified example of generating an API endpoint using this workflow. We’ll use natural language for our spec and a conceptual AI tool integrated into a GitHub Actions pipeline.
Our Goal: Create a Node.js Express endpoint GET /api/users/{userId}
that retrieves a user’s profile from a database.
First, we create a specification file, let’s call it get-user-profile.spec.md
.
# Feature: Get User Profile
## Endpoint
`GET /api/users/{userId}`
## Description
Retrieves the profile for a single user by their ID. The profile should only include the `id`, `name`, and `email` fields.
## Success Criteria
- **Scenario**: User is found
- Given a `userId` that exists in the database.
- When a GET request is made to `/api/users/{userId}`.
- Then the response status code should be 200.
- And the response body should be a JSON object containing the user's `id`, `name`, and `email`.
## Failure Criteria
- **Scenario**: User is not found
- Given a `userId` that does not exist in the database.
- When a GET request is made to `/api/users/{userId}`.
- Then the response status code should be 404.
- And the response body should contain an error message: "User not found".
Next, we feed this specification to an AI model to generate our tests. We might use a CLI tool or a custom script that calls an AI API.
Prompt:
“Based on get-user-profile.spec.md
, generate Jest and Supertest code for an Express.js application.”
AI-Generated Output (user.test.js
):
const request = require("supertest");
const express = require("express");
const {getUserById} = require("./userService");
// Mock the userService
jest.mock("./userService");
const app = express();
// Assume the router is set up similarly in the actual app
const userRouter = require("./userRouter");
app.use("/api/users", userRouter);
describe("GET /api/users/:userId", () => {
it("should return 200 and the user profile if the user exists", async () => {
const mockUser = {id: "123", name: "Alex Doe", email: "alex@example.com"};
getUserById.mockResolvedValue(mockUser);
const res = await request(app).get("/api/users/123");
expect(res.statusCode).toEqual(200);
expect(res.body).toEqual({
id: "123",
name: "Alex Doe",
email: "alex@example.com"
});
});
it("should return 404 if the user does not exist", async () => {
getUserById.mockResolvedValue(null);
const res = await request(app).get("/api/users/999");
expect(res.statusCode).toEqual(404);
expect(res.body.error).toEqual("User not found");
});
});
Now, we use the same spec to generate the application code.
Prompt:
“Based on get-user-profile.spec.md
, generate the Express.js router and handler function. Assume a userService
module handles database interaction.”
AI-Generated Output (userRouter.js
):
const express = require("express");
const {getUserById} = require("./userService");
const router = express.Router();
router.get("/:userId", async (req, res) => {
const {userId} = req.params;
try {
const user = await getUserById(userId);
if (!user) {
return res.status(404).json({error: "User not found"});
}
// Return only the specified fields
const userProfile = {
id: user.id,
name: user.name,
email: user.email
};
res.status(200).json(userProfile);
} catch (error) {
res.status(500).json({error: "Internal server error"});
}
});
module.exports = router;
Finally, we create a GitHub Actions workflow that runs the tests on every push.
.github/workflows/ci.yml
:
name: CI Pipeline
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test
With this setup, any change to the spec, code, or tests will trigger the pipeline, immediately notifying you if anything is out of sync.
Adopting a Spec → Test → Code
pipeline offers significant advantages for development teams.
- Reduces Drift: By generating tests and code from a single source of truth, you eliminate inconsistencies between what was requested, what was built, and how it was tested.
- Increases Velocity: AI automates the creation of boilerplate code and standard test cases, allowing engineers to focus on higher-value problems.
- Improves Quality: This process naturally leads to higher test coverage and ensures that every requirement is validated automatically.
- Enhances Collaboration: A clear, formal specification serves as a contract between product managers, developers, and QA, ensuring everyone is aligned.
While powerful, this AI-driven workflow comes with its own set of challenges.
Challenge | Mitigation Strategy |
---|---|
Vague or Ambiguous Specs | The “garbage in, garbage out” principle applies. Invest in training your team to write clear, precise, and unambiguous specifications. Use templates and linters for specs just as you would for code. |
AI “Hallucinations” | AI can sometimes generate incorrect or suboptimal code and tests. Always treat the AI’s output as a first draft. Human oversight and code review remain critical steps to ensure quality and security. |
Over-reliance on AI | Don’t let AI replace critical thinking. Developers should still understand the code and tests being generated, using the AI as a productivity tool, not a substitute for expertise. |
Tooling and Integration | Setting up the full pipeline can be complex. Start small by integrating AI assistance into one part of your workflow (e.g., test generation) and expand from there as your team becomes more comfortable. |
Addressing these challenges proactively ensures that you can harness the benefits of AI without falling victim to its potential downsides.
When building real-world applications, your specifications will inevitably involve core services like authentication, authorization, and user management. This is where a service like Kinde provides a massive advantage in an AI-driven pipeline.
Kinde’s well-documented and predictable APIs act as a perfect “spec” for an AI model. Instead of asking an AI to invent a secure authentication system, you can write a spec like:
“Scenario: User updates their profile. Given the user is authenticated with a valid JWT. When a PATCH
request is made to /api/me
. Then the application should use the Kinde Management API to update the user’s given_name
.”
The AI can then use Kinde’s SDKs and API documentation to generate the correct, secure code to handle that interaction. This approach combines the speed of AI generation with the security and reliability of a dedicated identity provider, letting you build complex, secure features faster than ever.
For more information, you can explore the Kinde Management API to see how its clear structure supports this development style.
Get started now
Boost security, drive conversion and save money — in just a few minutes.