Have you ever spent weeks building a backend API, only to have the frontend team tell you, "Wait, this isn't the data format we agreed on"?
In traditional "code-first" development, developers jump straight into writing logic. The documentation and contracts are treated as an afterthought. This leads to broken integrations, endless debugging, and frustrated teams. Enter Spec-Driven Development (SDD) (also known as API-First Design).
In SDD, you write the Specification (the contract) before you write any code. This spec becomes the single source of truth for your backend, frontend, and QA teams.
In this article, we will explore SDD using a real-world example: building a Task Management API using OpenAPI (Swagger), Node.js, and automating the validation process using GitHub Actions.
🏗️ The Real-World Example: A Task Management API
Imagine we are building a simple system to manage daily tasks. Instead of opening index.js and writing Express routes, we start by writing an OpenAPI specification.
Step 1: Writing the Specification (The Contract)
We define our API contract in a file called openapi.yaml. This file dictates exactly what the endpoints are, what data they require, and what they will return.
# openapi.yaml
openapi: 3.0.0
info:
title: "Task Management API"
version: 1.0.0
description: "A simple API to manage tasks, built with Spec-Driven Development."
paths:
/tasks:
get:
summary: Get all tasks
responses:
'200':
description: "A list of tasks"
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Task'
post:
summary: Create a new task
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewTask'
responses:
'201':
description: "Task created successfully"
components:
schemas:
Task:
type: object
required: [id, title, completed]
properties:
id:
type: string
format: uuid
title: ""
type: string
completed:
type: boolean
NewTask:
type: object
required: [title]
properties:
title: ""
type: string
minLength: 3
By writing this first, the frontend team can immediately start building the UI using mock servers, while the backend team starts implementing the logic.
Step 2: Implementation (The Code)
Now that we have our contract, we will write our backend. To make this truly Spec-Driven, we won't just write code that hopefully matches the spec; we will use a library called express-openapi-validator to force our code to obey the spec.
Here is the Node.js implementation:
// index.js
const express = require('express');
const path = require('path');
const OpenApiValidator = require('express-openapi-validator');
const app = express();
app.use(express.json());
// 1. Install the OpenAPI Validator middleware
// This automatically reads our openapi.yaml and validates all incoming requests
app.use(
OpenApiValidator.middleware({
apiSpec: path.join(__dirname, 'openapi.yaml'),
validateRequests: true, // Fail if request doesn't match spec
validateResponses: true // Fail if our server sends a bad response
})
);
// 2. Dummy Database
let tasks = [
{ id: "123e4567-e89b-12d3-a456-426614174000", title: "Learn SDD", completed: false }
];
// 3. Define Routes
app.get('/tasks', (req, res) => {
res.status(200).json(tasks);
});
app.post('/tasks', (req, res) => {
// We don't need to write validation logic like `if(!req.body.title)`!
// The OpenAPI validator already checked it against the YAML spec.
const newTask = {
id: "987e6543-e21b-34d3-b123-426614174111", // In reality, generate a UUID
title: req.body.title,
completed: false
};
tasks.push(newTask);
res.status(201).json(newTask);
});
// 4. Error Handler for Validation Errors
app.use((err, req, res, next) => {
res.status(err.status || 500).json({
message: err.message,
errors: err.errors,
});
});
app.listen(3000, () => {
console.log('Server running on port 3000. Protected by OpenAPI!');
});
Because of SDD, if a client tries to send {"title": "ab"} (which violates the minLength: 3 rule in our YAML), the server will automatically reject it with a 400 Bad Request. No extra code required!
Step 3: GitHub Version Control & Automation (CI/CD)
To achieve maximum efficiency (and max points for your assignment!), your code must live in a Version Control System (GitHub) with Automation.
The Repository Structure:
my-sdd-project/
├── .github/
│ └── workflows/
│ └── lint-spec.yml <-- CI/CD Automation
├── package.json
├── index.js <-- Application Code
└── openapi.yaml <-- The Specification
The Automation:
We want to ensure that nobody merges a broken API specification into the main branch. We can automate this using GitHub Actions and a tool called Spectral (a JSON/YAML linter for APIs).
Create a file at .github/workflows/lint-spec.yml:
name: Lint OpenAPI Spec
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
validate-spec:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install Spectral
run: npm install -g @stoplight/spectral-cli
- name: Lint OpenAPI Document
run: spectral lint openapi.yaml --ruleset spectral:oas
What does this automation do?
Every time a developer makes a Pull Request, GitHub Actions will spin up a server, read the openapi.yaml file, and check it for errors, missing descriptions, or bad formatting. If the spec is broken, the code cannot be merged.
Conclusion
Spec-Driven Development shifts the focus from writing code to designing architecture. By treating the Specification as a first-class citizen and automating its validation via CI/CD pipelines, teams can eliminate miscommunications, reduce bugs, and deliver higher-quality software significantly faster.
Remember: Design first, code second.
Top comments (0)