How I keep flow-state energy while shipping reliable features by writing the behavior first with Cucumber and Gherkin.
When I “vibe code,” my goal is simple: get into flow, keep the rhythm, and ship something real by the end of the session.
The problem? Raw momentum can trick me into building without clarity. Behavior-Driven Development (BDD) fixes that.
By writing the behavior first in plain English—Given / When / Then—I stay creative and grounded.
Below is a complete, minimal example of using Gherkin scenarios to build a tiny, testable, and shippable to-do list.
What We’ll Build
A minimal to-do core with behaviors for adding items, completing items, and filtering by all / active / completed.
We’ll write Gherkin scenarios first, wire step definitions with Cucumber.js, then implement just enough code to make them pass.
Project Setup (Node + Cucumber)
# 1) Initialize
npm init -y
# 2) Install Cucumber.js
npm i -D @cucumber/cucumber
# 3) Add a script in package.json
# "scripts": { "bdd": "cucumber-js" }
# 4) Create folders
mkdir -p features/steps src
Write the Behavior First (Gherkin)
Create features/todo.feature
:
Feature: Manage todos
As a focused user
I want to add and complete tasks
So that I can keep momentum without losing clarity
Background:
Given an empty todo list
Scenario: Add a single todo
When I add a todo titled "Buy milk"
Then I should see 1 todos
And I should see a todo titled "Buy milk" that is not completed
Scenario: Reject empty titles
When I add a todo titled ""
Then I should see an error "Title is required"
Scenario: Complete a todo
Given I add a todo titled "Stretch"
When I mark the todo titled "Stretch" as completed
Then the todo titled "Stretch" should be completed
Scenario Outline: Filter by status
Given I add a todo titled "Task A"
And I add a todo titled "Task B"
And I mark the todo titled "Task B" as completed
When I view the "<filter>" todos
Then I should see the titles: <expected>
Examples:
| filter | expected |
| all | Task A, Task B |
| active | Task A |
| completed | Task B |
Notice how the language is human-readable. If a step feels vague, refine the words until the behavior is unmistakable.
This is your spec, your anchor, and your groove.
Map Gherkin to Code (Step Definitions)
Create features/steps/todo.steps.js
:
const assert = require("assert");
const { Given, When, Then, Before } = require("@cucumber/cucumber");
const { createList, addTodo, toggleTodo, visibleTodos } = require("../../src/todo");
// Scenario-scoped context
Before(function () {
this.ctx = { list: createList(), error: null, lastResult: null };
});
Given("an empty todo list", function () {
// Already created in Before hook
assert.equal(this.ctx.list.items.length, 0);
});
Given("I add a todo titled {string}", function (title) {
try {
addTodo(this.ctx.list, title);
} catch (e) {
this.ctx.error = e;
}
});
When("I add a todo titled {string}", function (title) {
try {
addTodo(this.ctx.list, title);
} catch (e) {
this.ctx.error = e;
}
});
Then("I should see {int} todos", function (count) {
assert.equal(this.ctx.list.items.length, count);
});
Then("I should see a todo titled {string} that is not completed", function (title) {
const t = this.ctx.list.items.find(i => i.title === title);
assert.ok(t, `Could not find todo "${title}"`);
assert.equal(t.completed, false);
});
Then("I should see an error {string}", function (msg) {
assert.ok(this.ctx.error, "Expected an error but none occurred");
assert.equal(this.ctx.error.message, msg);
});
When("I mark the todo titled {string} as completed", function (title) {
const t = this.ctx.list.items.find(i => i.title === title);
assert.ok(t, `Could not find todo "${title}"`);
toggleTodo(this.ctx.list, t.id, true);
});
Then("the todo titled {string} should be completed", function (title) {
const t = this.ctx.list.items.find(i => i.title === title);
assert.ok(t, `Could not find todo "${title}"`);
assert.equal(t.completed, true);
});
When("I view the {string} todos", function (filter) {
this.ctx.lastResult = visibleTodos(this.ctx.list, filter);
});
Then("I should see the titles: {string}", function (csv) {
const expected = csv.split(",").map(s => s.trim()).filter(Boolean);
const actual = (this.ctx.lastResult || []).map(t => t.title);
assert.deepStrictEqual(actual, expected);
});
The step file is your “translation layer” from human language to code. We keep a tiny, in-memory “world” (this.ctx
) to
hold the list, last error, and last view result.
Make It Pass (Just-Enough Implementation)
Create src/todo.js
:
function createList() {
return { items: [], nextId: 1 };
}
function addTodo(list, rawTitle) {
const title = (rawTitle || "").trim();
if (!title) throw new Error("Title is required");
list.items.push({ id: list.nextId++, title, completed: false, createdAt: Date.now() });
return list;
}
function toggleTodo(list, id, completed) {
const t = list.items.find(i => i.id === id);
if (!t) throw new Error("Todo not found");
t.completed = Boolean(completed);
return t;
}
function visibleTodos(list, filter = "all") {
switch (filter) {
case "active":
return list.items.filter(i => !i.completed);
case "completed":
return list.items.filter(i => i.completed);
default:
return list.items.slice();
}
}
module.exports = { createList, addTodo, toggleTodo, visibleTodos };
Run the suite:
npm run bdd
You should see all scenarios green. If anything fails, tighten either the behavior (the words) or the implementation (the code)—but let the
tests lead.
Designing for Flow: The BDD Vibe Loop
- Set the groove (2–3 min): Choose the next single behavior. Write it in Gherkin. Read it out loud.
- Red: Run the suite and watch the new scenario fail for the right reason.
- Green: Implement the smallest code to pass. Avoid “future features.”
- Refactor: Clean naming and duplication once everything is green.
- Ship a slice: If this behavior is user-visible, deploy it. Tiny releases keep momentum.
This loop protects your focus. You keep the music on, keep the words tight, and let passing scenarios mark the downbeats.
Extend the App (More Scenarios, Same Rhythm)
When you’re ready, add more high-value behaviors as scenarios:
- Editing titles: Reject duplicates or very long titles.
- Bulk complete / clear completed: One-step flows for end-of-day cleanup.
- Persistence: Save and load from storage (file, DB, or browser
localStorage
). - Accessibility: Keyboard-only navigation and ARIA roles when you add a UI.
Keep each behavior shippable. Your future self (and your users) will feel the steady progress.
Optional: Tiny CLI to Demo the Core
If you want a super-light demo, wire a small CLI that uses the same domain functions. This keeps your tests
authoritative while giving you a tangible way to “feel” the app.
// src/cli.js (optional demo)
const readline = require("readline");
const { createList, addTodo, toggleTodo, visibleTodos } = require("./todo");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const list = createList();
function prompt() {
rl.question("> ", (line) => {
const [cmd, ...rest] = line.trim().split(" ");
if (cmd === "add") {
try { addTodo(list, rest.join(" ")); } catch (e) { console.log("Error:", e.message); }
} else if (cmd === "done") {
const title = rest.join(" ");
const t = list.items.find(i => i.title === title);
if (t) toggleTodo(list, t.id, true);
} else if (cmd === "view") {
const filter = rest[0] || "all";
console.table(visibleTodos(list, filter).map(t => ({ id: t.id, title: t.title, completed: t.completed })));
} else if (cmd === "exit") {
rl.close(); return;
} else {
console.log("Commands: add <title> | done <title> | view [all|active|completed] | exit");
}
prompt();
});
}
console.log("Commands: add <title> | done <title> | view [all|active|completed] | exit");
prompt();
Note: The CLI is just for vibes—your BDD suite remains the source of truth.
Why This Works for Vibe Coding
BDD forces clarity without killing momentum. Gherkin keeps conversations human, Cucumber makes them executable,
and your domain code stays small and purposeful. You can pause any time, come back later, and instantly recover context by
reading the feature file. That’s creative energy with a safety net.
Next Steps
Add one more scenario that matters to you (persistence, editing, or bulk actions). Keep the loop small, keep the music going,
and let green scenarios mark the beat. Ship tiny and often.