@vibe-interviewing/scenarios 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +5 -3
- package/package.json +7 -5
- package/patch-data-loss/scenario.yaml +161 -0
- package/registry.yaml +17 -5
- package/storage-adapter-refactor/scenario.yaml +118 -0
- package/webhook-notifications/scenario.yaml +163 -0
- package/rate-limiter-boundary/scenario.yaml +0 -71
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vibe-interviewing contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -4,9 +4,11 @@ Built-in interview scenarios for [vibe-interviewing](https://github.com/cpaczek/
|
|
|
4
4
|
|
|
5
5
|
## Available Scenarios
|
|
6
6
|
|
|
7
|
-
| Scenario
|
|
8
|
-
|
|
|
9
|
-
| `
|
|
7
|
+
| Scenario | Type | Difficulty | Time | Description |
|
|
8
|
+
| -------------------------- | -------- | ---------- | ------- | ------------------------------------------------------- |
|
|
9
|
+
| `patch-data-loss` | Debug | Hard | ~30-45m | PATCH requests silently drop fields from records |
|
|
10
|
+
| `storage-adapter-refactor` | Refactor | Medium | ~45-60m | Refactor tightly-coupled storage for pluggable backends |
|
|
11
|
+
| `webhook-notifications` | Feature | Hard | ~45-60m | Build a webhook notification system for a REST API |
|
|
10
12
|
|
|
11
13
|
## Creating Custom Scenarios
|
|
12
14
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibe-interviewing/scenarios",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Built-in interview scenarios for vibe-interviewing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
"index.js",
|
|
10
10
|
"index.d.ts",
|
|
11
11
|
"registry.yaml",
|
|
12
|
-
"
|
|
12
|
+
"patch-data-loss/",
|
|
13
|
+
"storage-adapter-refactor/",
|
|
14
|
+
"webhook-notifications/"
|
|
13
15
|
],
|
|
14
|
-
"scripts": {},
|
|
15
16
|
"license": "MIT",
|
|
16
17
|
"repository": {
|
|
17
18
|
"type": "git",
|
|
18
19
|
"url": "https://github.com/cpaczek/vibe-interviewing.git",
|
|
19
20
|
"directory": "packages/scenarios"
|
|
20
|
-
}
|
|
21
|
-
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {}
|
|
23
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
name: patch-data-loss
|
|
2
|
+
description: 'PATCH requests silently drop fields from records'
|
|
3
|
+
type: debug
|
|
4
|
+
difficulty: hard
|
|
5
|
+
estimated_time: '30-45m'
|
|
6
|
+
tags:
|
|
7
|
+
- node
|
|
8
|
+
- rest-api
|
|
9
|
+
- json-server
|
|
10
|
+
- data-integrity
|
|
11
|
+
|
|
12
|
+
repo: 'https://github.com/typicode/json-server'
|
|
13
|
+
commit: '6ad7562b6aba175bec616d342e1319e8dbbad1af'
|
|
14
|
+
|
|
15
|
+
setup:
|
|
16
|
+
- 'npm install'
|
|
17
|
+
|
|
18
|
+
patch:
|
|
19
|
+
# Replace the fixture data with richer records so field loss is observable
|
|
20
|
+
- file: 'fixtures/db.json'
|
|
21
|
+
find: |
|
|
22
|
+
{
|
|
23
|
+
"posts": [
|
|
24
|
+
{ "id": "1", "title": "a title" },
|
|
25
|
+
{ "id": "2", "title": "another title" }
|
|
26
|
+
],
|
|
27
|
+
"comments": [
|
|
28
|
+
{ "id": "1", "text": "a comment about post 1", "postId": "1" },
|
|
29
|
+
{ "id": "2", "text": "another comment about post 1", "postId": "1" }
|
|
30
|
+
],
|
|
31
|
+
"profile": {
|
|
32
|
+
"name": "typicode"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
replace: |
|
|
36
|
+
{
|
|
37
|
+
"products": [
|
|
38
|
+
{ "id": "1", "name": "Wireless Headphones", "price": 79.99, "category": "electronics", "description": "Noise-cancelling over-ear headphones", "inStock": true, "rating": 4.5 },
|
|
39
|
+
{ "id": "2", "name": "Running Shoes", "price": 129.99, "category": "footwear", "description": "Lightweight trail running shoes", "inStock": true, "rating": 4.2 },
|
|
40
|
+
{ "id": "3", "name": "Coffee Maker", "price": 49.99, "category": "kitchen", "description": "12-cup programmable coffee maker", "inStock": false, "rating": 3.8 },
|
|
41
|
+
{ "id": "4", "name": "Backpack", "price": 89.99, "category": "accessories", "description": "Water-resistant laptop backpack", "inStock": true, "rating": 4.7 }
|
|
42
|
+
],
|
|
43
|
+
"orders": [
|
|
44
|
+
{ "id": "1", "productId": "1", "quantity": 2, "status": "shipped", "customerEmail": "alice@example.com" },
|
|
45
|
+
{ "id": "2", "productId": "3", "quantity": 1, "status": "pending", "customerEmail": "bob@example.com" }
|
|
46
|
+
],
|
|
47
|
+
"settings": {
|
|
48
|
+
"storeName": "Demo Shop",
|
|
49
|
+
"currency": "USD",
|
|
50
|
+
"taxRate": 0.08
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Break PATCH merge for array resources (collections like /products/:id)
|
|
55
|
+
# Remove the spread of existing item, making PATCH behave like PUT
|
|
56
|
+
- file: 'src/service.ts'
|
|
57
|
+
find: 'const nextItem = isPatch ? { ...item, ...body, id } : { ...body, id }'
|
|
58
|
+
replace: 'const nextItem = { ...body, id }'
|
|
59
|
+
|
|
60
|
+
delete_files:
|
|
61
|
+
- 'src/*.test.ts'
|
|
62
|
+
|
|
63
|
+
briefing: |
|
|
64
|
+
We've got a P1 from the support team. Multiple customers are reporting data loss
|
|
65
|
+
in our product catalog API. They say they'll update a single field on a product —
|
|
66
|
+
like changing the price — and when they fetch it back, all the other fields are gone.
|
|
67
|
+
Just the field they sent plus the ID.
|
|
68
|
+
|
|
69
|
+
It's not happening on every endpoint, and some customers aren't seeing it at all.
|
|
70
|
+
Nobody's sure when this started but it's been in the queue for a few days now.
|
|
71
|
+
|
|
72
|
+
The API is built on json-server. You can start it locally with:
|
|
73
|
+
|
|
74
|
+
node --experimental-strip-types src/bin.ts fixtures/db.json
|
|
75
|
+
|
|
76
|
+
Then hit it with curl on port 3000. The data is in fixtures/db.json.
|
|
77
|
+
Start by reproducing the issue, then trace through the code to find the root cause.
|
|
78
|
+
|
|
79
|
+
ai_rules:
|
|
80
|
+
role: |
|
|
81
|
+
You are a senior engineer helping a candidate debug a data loss issue
|
|
82
|
+
in a REST API built on json-server. Act as a thoughtful colleague —
|
|
83
|
+
available for questions but you don't volunteer the answer.
|
|
84
|
+
rules:
|
|
85
|
+
- 'Never reveal the exact location or nature of the bug directly'
|
|
86
|
+
- 'If asked, confirm whether the candidate is looking in the right area'
|
|
87
|
+
- 'Encourage the candidate to reproduce the issue first with curl requests'
|
|
88
|
+
- 'If the candidate is stuck, suggest they compare what happens with PUT vs PATCH'
|
|
89
|
+
- 'If stuck for more than 15 minutes, hint that the issue might be in how updates merge with existing data'
|
|
90
|
+
- 'Praise good debugging methodology (reproducing first, reading source, adding logging)'
|
|
91
|
+
knowledge: |
|
|
92
|
+
The bug is in src/service.ts in the #updateOrPatchById method.
|
|
93
|
+
The original code was:
|
|
94
|
+
const nextItem = isPatch ? { ...item, ...body, id } : { ...body, id }
|
|
95
|
+
It was changed to:
|
|
96
|
+
const nextItem = { ...body, id }
|
|
97
|
+
This removes the ternary that differentiated PATCH from PUT. Now PATCH
|
|
98
|
+
behaves like PUT — it replaces the entire object with just the body fields
|
|
99
|
+
plus the ID, instead of merging the body into the existing object.
|
|
100
|
+
The fix is to restore the ternary:
|
|
101
|
+
const nextItem = isPatch ? { ...item, ...body, id } : { ...body, id }
|
|
102
|
+
Note: the non-array #updateOrPatch method (for singular resources like /settings)
|
|
103
|
+
is NOT affected — it still correctly merges with { ...item, ...body }.
|
|
104
|
+
|
|
105
|
+
solution: |
|
|
106
|
+
In src/service.ts, in the #updateOrPatchById method (around line 178), change:
|
|
107
|
+
const nextItem = { ...body, id }
|
|
108
|
+
back to:
|
|
109
|
+
const nextItem = isPatch ? { ...item, ...body, id } : { ...body, id }
|
|
110
|
+
|
|
111
|
+
The bug made PATCH identical to PUT for collection items. PATCH should merge
|
|
112
|
+
the request body into the existing item (preserving fields not in the body),
|
|
113
|
+
while PUT should replace the entire item. The fix restores the spread of the
|
|
114
|
+
existing item when isPatch is true.
|
|
115
|
+
|
|
116
|
+
evaluation:
|
|
117
|
+
criteria:
|
|
118
|
+
- 'Reproduced the data loss issue with curl requests'
|
|
119
|
+
- 'Identified that PATCH is dropping fields not included in the request body'
|
|
120
|
+
- 'Located the bug in the service layer (src/service.ts)'
|
|
121
|
+
- 'Understood the difference between PATCH (merge) and PUT (replace) semantics'
|
|
122
|
+
- 'Applied the correct fix restoring the merge behavior'
|
|
123
|
+
- 'Verified the fix works by re-testing with curl'
|
|
124
|
+
- 'Used AI effectively as a debugging partner'
|
|
125
|
+
expected_fix: 'Restore the ternary in #updateOrPatchById: isPatch ? { ...item, ...body, id } : { ...body, id }'
|
|
126
|
+
|
|
127
|
+
interviewer_guide:
|
|
128
|
+
overview: |
|
|
129
|
+
This scenario evaluates systematic debugging skills and HTTP semantics understanding.
|
|
130
|
+
The bug is subtle — PATCH was changed to behave like PUT, dropping fields not included
|
|
131
|
+
in the request body. Strong candidates reproduce first, read the source, and understand
|
|
132
|
+
the merge vs replace distinction. This also tests how candidates use AI as a debugging
|
|
133
|
+
partner rather than just asking it to find the bug.
|
|
134
|
+
key_signals:
|
|
135
|
+
- signal: 'Reproduces before fixing'
|
|
136
|
+
positive: 'Starts by sending curl requests to reproduce the data loss'
|
|
137
|
+
negative: 'Jumps straight into reading code or asks AI to find the bug'
|
|
138
|
+
- signal: 'Reads source code'
|
|
139
|
+
positive: 'Traces the request path through the codebase to find where updates happen'
|
|
140
|
+
negative: 'Relies entirely on AI to explain the code without reading it'
|
|
141
|
+
- signal: 'Understands HTTP semantics'
|
|
142
|
+
positive: 'Recognizes that PATCH should merge and PUT should replace'
|
|
143
|
+
negative: 'Treats PATCH and PUT as interchangeable or does not know the difference'
|
|
144
|
+
- signal: 'Critical evaluation of AI'
|
|
145
|
+
positive: 'Questions AI suggestions, verifies them against the actual code'
|
|
146
|
+
negative: 'Accepts the first AI suggestion without testing or reviewing'
|
|
147
|
+
- signal: 'Verifies the fix'
|
|
148
|
+
positive: 'Re-runs the reproduction steps to confirm the fix works'
|
|
149
|
+
negative: 'Applies a fix and considers it done without testing'
|
|
150
|
+
common_pitfalls:
|
|
151
|
+
- 'Asking AI to "find the bug" without investigating — shows over-reliance on AI'
|
|
152
|
+
- 'Fixing symptoms (e.g., adding a workaround in the route handler) instead of the root cause'
|
|
153
|
+
- 'Not understanding that the non-array #updateOrPatch method is NOT affected'
|
|
154
|
+
- 'Confusing the spread operator behavior — the fix is about restoring the existing item spread'
|
|
155
|
+
debrief_questions:
|
|
156
|
+
- 'Walk me through how you reproduced the issue. What requests did you try?'
|
|
157
|
+
- 'Can you explain the difference between how PATCH and PUT should behave?'
|
|
158
|
+
- 'How did you use AI during the debugging process? What worked well?'
|
|
159
|
+
- 'If you were reviewing a PR that introduced this bug, what would you look for?'
|
|
160
|
+
|
|
161
|
+
license: MIT
|
package/registry.yaml
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
scenarios:
|
|
2
|
-
- name:
|
|
3
|
-
description:
|
|
4
|
-
difficulty:
|
|
2
|
+
- name: patch-data-loss
|
|
3
|
+
description: 'PATCH requests silently drop fields from records'
|
|
4
|
+
difficulty: hard
|
|
5
5
|
estimated_time: '30-45m'
|
|
6
|
-
repo: 'https://github.com/
|
|
7
|
-
commit: '
|
|
6
|
+
repo: 'https://github.com/typicode/json-server'
|
|
7
|
+
commit: '6ad7562b6aba175bec616d342e1319e8dbbad1af'
|
|
8
|
+
- name: storage-adapter-refactor
|
|
9
|
+
description: 'Refactor tightly-coupled storage layer to support pluggable backends'
|
|
10
|
+
difficulty: medium
|
|
11
|
+
estimated_time: '45-60m'
|
|
12
|
+
repo: 'https://github.com/typicode/json-server'
|
|
13
|
+
commit: '6ad7562b6aba175bec616d342e1319e8dbbad1af'
|
|
14
|
+
- name: webhook-notifications
|
|
15
|
+
description: 'Build a webhook notification system for a REST API'
|
|
16
|
+
difficulty: hard
|
|
17
|
+
estimated_time: '45-60m'
|
|
18
|
+
repo: 'https://github.com/typicode/json-server'
|
|
19
|
+
commit: '6ad7562b6aba175bec616d342e1319e8dbbad1af'
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
name: storage-adapter-refactor
|
|
2
|
+
description: 'Refactor tightly-coupled storage layer to support pluggable backends'
|
|
3
|
+
type: refactor
|
|
4
|
+
difficulty: medium
|
|
5
|
+
estimated_time: '45-60m'
|
|
6
|
+
tags:
|
|
7
|
+
- node
|
|
8
|
+
- rest-api
|
|
9
|
+
- json-server
|
|
10
|
+
- architecture
|
|
11
|
+
- design-patterns
|
|
12
|
+
|
|
13
|
+
repo: 'https://github.com/typicode/json-server'
|
|
14
|
+
commit: '6ad7562b6aba175bec616d342e1319e8dbbad1af'
|
|
15
|
+
|
|
16
|
+
setup:
|
|
17
|
+
- 'npm install'
|
|
18
|
+
|
|
19
|
+
delete_files:
|
|
20
|
+
- 'src/*.test.ts'
|
|
21
|
+
|
|
22
|
+
briefing: |
|
|
23
|
+
We're turning json-server into a more flexible tool. Right now the entire data
|
|
24
|
+
layer is hardcoded to lowdb — the Service class directly imports and uses Low<Data>,
|
|
25
|
+
reads/writes through this.#db.data, and calls this.#db.write(). This coupling means
|
|
26
|
+
anyone who wants to use a different storage backend (in-memory for tests, SQLite for
|
|
27
|
+
persistence, Redis for shared state) has to fork the whole thing.
|
|
28
|
+
|
|
29
|
+
Your job: refactor the data layer so storage is pluggable. Define a clean storage
|
|
30
|
+
interface, wrap the existing lowdb usage in an adapter that implements it, and
|
|
31
|
+
refactor the Service class to depend on the interface instead of lowdb directly.
|
|
32
|
+
Then create an in-memory adapter as a second implementation to prove the
|
|
33
|
+
interface works.
|
|
34
|
+
|
|
35
|
+
The server runs locally:
|
|
36
|
+
|
|
37
|
+
node --experimental-strip-types src/bin.ts fixtures/db.json
|
|
38
|
+
|
|
39
|
+
Make sure the server still works exactly the same with the JSON file adapter
|
|
40
|
+
after your refactoring. You can test with curl on port 3000.
|
|
41
|
+
|
|
42
|
+
ai_rules:
|
|
43
|
+
role: |
|
|
44
|
+
You are a senior engineer helping a candidate refactor a storage layer.
|
|
45
|
+
Guide them on design decisions but don't dictate the architecture.
|
|
46
|
+
rules:
|
|
47
|
+
- 'Let the candidate drive the interface design — there are multiple valid approaches'
|
|
48
|
+
- 'If asked, discuss trade-offs between different abstraction levels'
|
|
49
|
+
- 'Encourage them to keep the interface minimal — only what Service actually needs'
|
|
50
|
+
- 'Suggest they start by identifying all the places Service touches the database'
|
|
51
|
+
- 'If they over-engineer (too many methods, complex generics), gently suggest simplifying'
|
|
52
|
+
- 'Remind them to verify the server still works after each refactoring step'
|
|
53
|
+
knowledge: |
|
|
54
|
+
The Service class in src/service.ts directly depends on Low<Data> from lowdb.
|
|
55
|
+
It accesses data through this.#db.data (a Record<string, Item[] | Item>) and
|
|
56
|
+
persists changes with this.#db.write(). The key coupling points are:
|
|
57
|
+
- Constructor takes Low<Data>
|
|
58
|
+
- #get reads from this.#db.data[name]
|
|
59
|
+
- create/update/patch/destroy all call this.#db.write() after mutations
|
|
60
|
+
- The embed/nullifyForeignKey/deleteDependents functions also access db.data
|
|
61
|
+
|
|
62
|
+
A good refactoring would:
|
|
63
|
+
1. Define a StorageAdapter interface with methods like get(name), set(name, data), write()
|
|
64
|
+
2. Create a LowdbAdapter that wraps the current Low<Data> behavior
|
|
65
|
+
3. Create an InMemoryAdapter that stores data in a plain object
|
|
66
|
+
4. Update Service to accept StorageAdapter instead of Low<Data>
|
|
67
|
+
5. Update app.ts/bin.ts to create the adapter and pass it through
|
|
68
|
+
|
|
69
|
+
The candidate should notice that the helper functions (embed, nullifyForeignKey,
|
|
70
|
+
deleteDependents) also access db.data directly, which is a design decision —
|
|
71
|
+
either move them into Service or make them work through the adapter.
|
|
72
|
+
|
|
73
|
+
evaluation:
|
|
74
|
+
criteria:
|
|
75
|
+
- 'Identified all coupling points between Service and lowdb'
|
|
76
|
+
- 'Designed a clean, minimal storage interface'
|
|
77
|
+
- 'Implemented a lowdb adapter that preserves existing behavior'
|
|
78
|
+
- 'Implemented an in-memory adapter'
|
|
79
|
+
- 'Refactored Service to use the interface instead of lowdb directly'
|
|
80
|
+
- 'Server still works correctly after refactoring (verified with curl)'
|
|
81
|
+
- 'Made thoughtful design decisions about abstraction level'
|
|
82
|
+
- 'Used AI effectively as an architecture partner'
|
|
83
|
+
|
|
84
|
+
interviewer_guide:
|
|
85
|
+
overview: |
|
|
86
|
+
This scenario evaluates architectural thinking and incremental refactoring skills.
|
|
87
|
+
The candidate must decouple a tightly-coupled storage layer (lowdb) from the Service
|
|
88
|
+
class by introducing a clean interface. There is no single right answer — multiple
|
|
89
|
+
valid approaches exist. Focus on how the candidate reasons about abstraction boundaries,
|
|
90
|
+
makes trade-offs, and refactors incrementally without breaking existing behavior.
|
|
91
|
+
key_signals:
|
|
92
|
+
- signal: 'Identifies coupling points first'
|
|
93
|
+
positive: 'Maps out all places Service touches lowdb before writing any code'
|
|
94
|
+
negative: 'Starts coding the interface without understanding the existing dependencies'
|
|
95
|
+
- signal: 'Designs a minimal interface'
|
|
96
|
+
positive: 'Creates an interface with only the methods Service actually needs'
|
|
97
|
+
negative: 'Over-engineers with many generic methods, complex generics, or framework-like abstractions'
|
|
98
|
+
- signal: 'Refactors incrementally'
|
|
99
|
+
positive: 'Makes one change at a time and verifies the server still works after each step'
|
|
100
|
+
negative: 'Attempts a big-bang rewrite — changes everything at once and debugs the result'
|
|
101
|
+
- signal: 'Handles helper functions'
|
|
102
|
+
positive: 'Notices that embed/nullifyForeignKey/deleteDependents also access db.data and has a plan for them'
|
|
103
|
+
negative: 'Misses the helper functions entirely, leaving them broken after refactoring'
|
|
104
|
+
- signal: 'Uses AI as architecture partner'
|
|
105
|
+
positive: 'Discusses design trade-offs with AI, considers alternatives before committing'
|
|
106
|
+
negative: 'Asks AI to generate the entire refactoring without engaging in design decisions'
|
|
107
|
+
common_pitfalls:
|
|
108
|
+
- 'Over-engineering the interface with methods that are not needed (YAGNI violation)'
|
|
109
|
+
- 'Forgetting the helper functions (embed, nullifyForeignKey, deleteDependents) that also access db.data directly'
|
|
110
|
+
- 'Not verifying the server still works after refactoring — the server must pass the same curl tests'
|
|
111
|
+
- 'Creating a leaky abstraction where lowdb internals bleed through the interface'
|
|
112
|
+
debrief_questions:
|
|
113
|
+
- 'How did you decide what methods to put on the storage interface?'
|
|
114
|
+
- 'What trade-offs did you consider when designing the abstraction?'
|
|
115
|
+
- 'How did you handle the helper functions that also access the database directly?'
|
|
116
|
+
- 'If you had more time, what would you improve about your design?'
|
|
117
|
+
|
|
118
|
+
license: MIT
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
name: webhook-notifications
|
|
2
|
+
description: 'Build a webhook notification system for a REST API'
|
|
3
|
+
type: feature
|
|
4
|
+
difficulty: hard
|
|
5
|
+
estimated_time: '45-60m'
|
|
6
|
+
tags:
|
|
7
|
+
- node
|
|
8
|
+
- rest-api
|
|
9
|
+
- json-server
|
|
10
|
+
- webhooks
|
|
11
|
+
- event-driven
|
|
12
|
+
|
|
13
|
+
repo: 'https://github.com/typicode/json-server'
|
|
14
|
+
commit: '6ad7562b6aba175bec616d342e1319e8dbbad1af'
|
|
15
|
+
|
|
16
|
+
setup:
|
|
17
|
+
- 'npm install'
|
|
18
|
+
|
|
19
|
+
patch:
|
|
20
|
+
# Replace fixture data with richer records for more interesting webhook payloads
|
|
21
|
+
- file: 'fixtures/db.json'
|
|
22
|
+
find: |
|
|
23
|
+
{
|
|
24
|
+
"posts": [
|
|
25
|
+
{ "id": "1", "title": "a title" },
|
|
26
|
+
{ "id": "2", "title": "another title" }
|
|
27
|
+
],
|
|
28
|
+
"comments": [
|
|
29
|
+
{ "id": "1", "text": "a comment about post 1", "postId": "1" },
|
|
30
|
+
{ "id": "2", "text": "another comment about post 1", "postId": "1" }
|
|
31
|
+
],
|
|
32
|
+
"profile": {
|
|
33
|
+
"name": "typicode"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
replace: |
|
|
37
|
+
{
|
|
38
|
+
"products": [
|
|
39
|
+
{ "id": "1", "name": "Wireless Headphones", "price": 79.99, "category": "electronics", "inStock": true },
|
|
40
|
+
{ "id": "2", "name": "Running Shoes", "price": 129.99, "category": "footwear", "inStock": true },
|
|
41
|
+
{ "id": "3", "name": "Coffee Maker", "price": 49.99, "category": "kitchen", "inStock": false }
|
|
42
|
+
],
|
|
43
|
+
"orders": [
|
|
44
|
+
{ "id": "1", "productId": "1", "quantity": 2, "status": "shipped" },
|
|
45
|
+
{ "id": "2", "productId": "3", "quantity": 1, "status": "pending" }
|
|
46
|
+
],
|
|
47
|
+
"webhooks": []
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
delete_files:
|
|
51
|
+
- 'src/*.test.ts'
|
|
52
|
+
|
|
53
|
+
briefing: |
|
|
54
|
+
We need to add webhook support to our product catalog API so that external
|
|
55
|
+
integrations can react to changes in real time. Right now if a third-party system
|
|
56
|
+
wants to know when a product is updated or an order is created, they have to poll.
|
|
57
|
+
|
|
58
|
+
Build a webhook notification system:
|
|
59
|
+
|
|
60
|
+
1. Clients register webhooks by POSTing to /webhooks with a URL and optional
|
|
61
|
+
event filter (e.g., only notify on "products" changes)
|
|
62
|
+
2. When any resource is created, updated, or deleted, the server sends a POST
|
|
63
|
+
request to all matching registered webhook URLs
|
|
64
|
+
3. Clients can list their webhooks (GET /webhooks) and delete them (DELETE /webhooks/:id)
|
|
65
|
+
|
|
66
|
+
The webhook payload should include what happened: the event type (create/update/delete),
|
|
67
|
+
which resource, and the affected data.
|
|
68
|
+
|
|
69
|
+
The server runs locally:
|
|
70
|
+
|
|
71
|
+
node --experimental-strip-types src/bin.ts fixtures/db.json
|
|
72
|
+
|
|
73
|
+
Test with curl on port 3000. You can use a simple listener (like a second
|
|
74
|
+
HTTP server or a request-catching service) to verify webhook delivery.
|
|
75
|
+
|
|
76
|
+
acceptance_criteria:
|
|
77
|
+
- 'POST /webhooks with { "url": "...", "resource": "products" } registers a webhook'
|
|
78
|
+
- 'POST /webhooks with { "url": "..." } (no resource filter) registers for all events'
|
|
79
|
+
- 'GET /webhooks returns all registered webhooks'
|
|
80
|
+
- 'DELETE /webhooks/:id removes a webhook'
|
|
81
|
+
- 'When a resource is created (POST /products), matching webhooks receive a notification'
|
|
82
|
+
- 'When a resource is updated (PUT or PATCH /products/:id), matching webhooks receive a notification'
|
|
83
|
+
- 'When a resource is deleted (DELETE /products/:id), matching webhooks receive a notification'
|
|
84
|
+
- 'Webhook payload includes: event type, resource name, affected data, and timestamp'
|
|
85
|
+
- 'Failed webhook deliveries do not crash the server or block the API response'
|
|
86
|
+
- 'Existing CRUD endpoints still work correctly'
|
|
87
|
+
|
|
88
|
+
ai_rules:
|
|
89
|
+
role: |
|
|
90
|
+
You are a senior engineer helping a candidate build a webhook notification
|
|
91
|
+
system. Guide their design decisions but let them drive the implementation.
|
|
92
|
+
rules:
|
|
93
|
+
- 'Help the candidate understand the existing codebase structure when asked'
|
|
94
|
+
- 'If asked about design choices, discuss trade-offs (sync vs async delivery, retry strategies, etc.)'
|
|
95
|
+
- 'Encourage starting with the simplest working implementation, then iterating'
|
|
96
|
+
- 'Suggest they write a small test listener to verify webhook delivery'
|
|
97
|
+
- 'If they get bogged down in edge cases (retries, backoff, ordering), redirect to core functionality first'
|
|
98
|
+
- 'Praise clean integration with the existing codebase patterns'
|
|
99
|
+
knowledge: |
|
|
100
|
+
The json-server codebase has a clean separation:
|
|
101
|
+
- src/app.ts defines routes and delegates to Service
|
|
102
|
+
- src/service.ts handles CRUD logic with lowdb
|
|
103
|
+
- The "webhooks" array is already seeded in db.json as an empty array
|
|
104
|
+
|
|
105
|
+
A good implementation would:
|
|
106
|
+
1. Use the existing Service class to manage webhook registrations (CRUD on /webhooks)
|
|
107
|
+
— json-server already handles any resource in db.json, so GET/POST/DELETE /webhooks
|
|
108
|
+
works out of the box. The candidate might realize this or build custom handlers.
|
|
109
|
+
2. Add middleware or post-response hooks in app.ts that fire after mutations
|
|
110
|
+
3. Use fetch() to deliver webhook payloads asynchronously (fire-and-forget)
|
|
111
|
+
4. Structure the payload as: { event: "create"|"update"|"delete", resource: "products", data: {...}, timestamp: "..." }
|
|
112
|
+
|
|
113
|
+
Key insight: since "webhooks" is already a resource in db.json, the candidate
|
|
114
|
+
gets CRUD for webhook registrations for free via json-server's existing routing.
|
|
115
|
+
They just need to add the notification delivery logic.
|
|
116
|
+
|
|
117
|
+
evaluation:
|
|
118
|
+
criteria:
|
|
119
|
+
- 'Webhook registration works (create, list, delete)'
|
|
120
|
+
- 'Notifications are delivered on create, update, and delete operations'
|
|
121
|
+
- 'Webhook payload contains event type, resource, data, and timestamp'
|
|
122
|
+
- 'Resource filtering works (webhooks can subscribe to specific resources)'
|
|
123
|
+
- 'Failed deliveries are handled gracefully (no crashes, no blocking)'
|
|
124
|
+
- 'Clean integration with existing codebase patterns'
|
|
125
|
+
- 'Tested end-to-end with a webhook listener'
|
|
126
|
+
- 'Used AI effectively as a development partner'
|
|
127
|
+
|
|
128
|
+
interviewer_guide:
|
|
129
|
+
overview: |
|
|
130
|
+
This scenario evaluates feature design and integration skills. The candidate must build
|
|
131
|
+
a webhook notification system on top of an existing REST API. A key insight is that
|
|
132
|
+
json-server already handles CRUD for any resource in db.json — so the "webhooks" array
|
|
133
|
+
gives them registration for free. Strong candidates discover this quickly and focus their
|
|
134
|
+
effort on the notification delivery logic rather than rebuilding CRUD.
|
|
135
|
+
key_signals:
|
|
136
|
+
- signal: 'Discovers the free CRUD'
|
|
137
|
+
positive: 'Realizes json-server already handles /webhooks CRUD via the seeded array in db.json'
|
|
138
|
+
negative: 'Builds custom webhook registration routes from scratch, duplicating existing functionality'
|
|
139
|
+
- signal: 'Starts with core functionality'
|
|
140
|
+
positive: 'Gets basic webhook delivery working first, then adds filtering and error handling'
|
|
141
|
+
negative: 'Gets bogged down in edge cases (retries, backoff, ordering) before core delivery works'
|
|
142
|
+
- signal: 'Handles errors gracefully'
|
|
143
|
+
positive: 'Webhook delivery failures are caught and do not crash the server or block API responses'
|
|
144
|
+
negative: 'Unhandled promise rejections or synchronous delivery that blocks the response'
|
|
145
|
+
- signal: 'Tests end-to-end'
|
|
146
|
+
positive: 'Sets up a listener to verify webhook delivery, tests the full flow'
|
|
147
|
+
negative: 'Only tests registration without verifying actual delivery'
|
|
148
|
+
- signal: 'Clean integration'
|
|
149
|
+
positive: 'Hooks into existing codebase patterns (middleware, service methods) rather than bolting on'
|
|
150
|
+
negative: 'Adds webhook logic in awkward places that break separation of concerns'
|
|
151
|
+
common_pitfalls:
|
|
152
|
+
- "Building custom CRUD for webhooks instead of leveraging json-server's built-in resource handling"
|
|
153
|
+
- 'Making webhook delivery synchronous, which blocks the API response'
|
|
154
|
+
- 'Over-engineering retry logic and backoff strategies before basic delivery works'
|
|
155
|
+
- 'Not filtering notifications by resource — sending every event to every webhook'
|
|
156
|
+
- 'Forgetting to exclude the "webhooks" resource from triggering webhook notifications'
|
|
157
|
+
debrief_questions:
|
|
158
|
+
- 'How did you decide where to hook into the codebase for notification delivery?'
|
|
159
|
+
- 'Did you notice that json-server handles /webhooks CRUD automatically? How did that affect your approach?'
|
|
160
|
+
- 'How would you handle webhook delivery at scale if this were a production system?'
|
|
161
|
+
- 'What would you add if you had another 30 minutes?'
|
|
162
|
+
|
|
163
|
+
license: MIT
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
name: rate-limiter-boundary
|
|
2
|
-
description: "Off-by-one in express-rate-limit's sliding window lets one extra request through"
|
|
3
|
-
difficulty: medium
|
|
4
|
-
estimated_time: '30-45m'
|
|
5
|
-
tags:
|
|
6
|
-
- node
|
|
7
|
-
- express
|
|
8
|
-
- middleware
|
|
9
|
-
- rate-limiting
|
|
10
|
-
- off-by-one
|
|
11
|
-
|
|
12
|
-
repo: 'https://github.com/express-rate-limit/express-rate-limit'
|
|
13
|
-
commit: '4e8b18bf972eff2890ed67bd11d8a08a2c6502d5'
|
|
14
|
-
|
|
15
|
-
setup:
|
|
16
|
-
- 'npm install --ignore-scripts'
|
|
17
|
-
|
|
18
|
-
patch:
|
|
19
|
-
- file: 'source/rate-limit.ts'
|
|
20
|
-
find: 'if (totalHits > limit) {'
|
|
21
|
-
replace: 'if (totalHits > limit + 1) {'
|
|
22
|
-
|
|
23
|
-
briefing: |
|
|
24
|
-
Hey — we're getting reports from a few customers that our rate limiting isn't working correctly. They're saying they can make one more request than the configured limit before getting a 429.
|
|
25
|
-
|
|
26
|
-
For example, if the limit is set to 5, clients can make 6 successful requests before being blocked. It's not a huge deal but it's inconsistent and a couple of enterprise customers have flagged it in their security audits.
|
|
27
|
-
|
|
28
|
-
We're using express-rate-limit. I've already cloned the repo and set it up locally — can you dig into the source and figure out what's going on?
|
|
29
|
-
|
|
30
|
-
To run the tests: `npx jest --no-coverage`
|
|
31
|
-
|
|
32
|
-
The main source is in `source/` and tests are in `test/library/`. Good luck!
|
|
33
|
-
|
|
34
|
-
ai_rules:
|
|
35
|
-
role: |
|
|
36
|
-
You are a senior engineer helping a candidate debug an off-by-one error
|
|
37
|
-
in the express-rate-limit middleware. Act as a patient but not overly
|
|
38
|
-
helpful colleague — you're available for questions but you don't
|
|
39
|
-
volunteer the answer.
|
|
40
|
-
rules:
|
|
41
|
-
- 'Never reveal the exact location or nature of the bug directly'
|
|
42
|
-
- 'If asked, confirm whether the candidate is looking in the right area'
|
|
43
|
-
- 'Encourage the candidate to write or run tests to reproduce the issue'
|
|
44
|
-
- 'If the candidate is stuck for more than 10 minutes, suggest looking at where totalHits is compared to the limit'
|
|
45
|
-
- 'Praise good debugging methodology (reading tests, adding logging, bisecting)'
|
|
46
|
-
knowledge: |
|
|
47
|
-
The bug is on line 507 of source/rate-limit.ts. The comparison
|
|
48
|
-
`if (totalHits > limit)` was changed to `if (totalHits > limit + 1)`.
|
|
49
|
-
This means the rate limiter allows limit+1 requests instead of limit
|
|
50
|
-
requests before returning 429. The fix is to change it back to
|
|
51
|
-
`if (totalHits > limit)`.
|
|
52
|
-
|
|
53
|
-
solution: |
|
|
54
|
-
In source/rate-limit.ts line 507, change:
|
|
55
|
-
`if (totalHits > limit + 1) {`
|
|
56
|
-
back to:
|
|
57
|
-
`if (totalHits > limit) {`
|
|
58
|
-
|
|
59
|
-
The off-by-one was introduced by adding `+ 1` to the limit comparison,
|
|
60
|
-
allowing one extra request through before rate limiting kicks in.
|
|
61
|
-
|
|
62
|
-
evaluation:
|
|
63
|
-
criteria:
|
|
64
|
-
- 'Identified the bug location in source/rate-limit.ts'
|
|
65
|
-
- 'Understood the off-by-one nature of the bug'
|
|
66
|
-
- 'Used tests or manual testing to reproduce the issue'
|
|
67
|
-
- 'Applied the correct fix'
|
|
68
|
-
- 'Used AI effectively as a debugging partner'
|
|
69
|
-
expected_fix: 'Change `totalHits > limit + 1` back to `totalHits > limit`'
|
|
70
|
-
|
|
71
|
-
license: MIT
|