Start / Skills / Entwickler-Produktivität / Implementierungspläne schreiben (Writing Plans)
Entwickler-Produktivität

Implementierungspläne schreiben (Writing Plans)

Jesse VincentMITSprache des Skills: Englisch
Lizenz geprüftKeine SkripteMittleres Risiko
DemoEchter Lauf · claude-sonnet-5-5 · Oktober 2026
Using the writing-plans skill, write an implementation plan for this fictional spec: add rate limiting to a small Express API. Limit each IP to 100 requests per minute, return HTTP 429 with a Retry-After header when exceeded, make the limit and window configurable through environment variables, use an in-memory store, and cover it with Jest tests. The project has src/app.js and tests/ already. You cannot write files here, so print the plan.
Skill: writing-plans

I'm using the writing-plans skill to create the implementation plan. I can't write files here, so the plan is printed below. Its intended path is docs/superpowers/plans/2026-10-05-rate-limiting.md.


Rate Limiting Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Limit each client IP to 100 requests per minute on the Express API, answering excess requests with HTTP 429 and a Retry-After header.

Architecture: A small middleware factory in src/rateLimiter.js keeps a per-IP fixed-window counter in an in-memory Map. A separate src/config.js reads the limit and window from environment variables. src/app.js mounts the middleware before all routes.

Tech Stack: Node.js, Express, Jest, supertest. Supertest is added as a devDependency if it is not already present.

Spec: The fictional spec was given inline in the request. No spec file exists.

Global Constraints

  • Default limit is 100 requests per IP per window.
  • Default window is 60 seconds (1 minute).
  • A request over the limit gets HTTP 429 with a Retry-After header.
  • The limit and window are configurable through environment variables.
  • The store is in-memory, with no external dependencies such as Redis.
  • Tests use Jest.

Review Focus

  1. Window expiry: after the window passes, the same IP is allowed again and its counter restarts. Pinned in Task 2.
  2. Independent IPs: one IP exhausting its limit must not block another IP. Pinned in Task 2.
  3. Invalid env values (RATE_LIMIT_MAX=abc, 0, negative, empty): fall back to the defaults instead of producing NaN and silently disabling or blocking everything. Pinned in Task 1.
  4. Retry-After accuracy: the value is a positive integer in seconds, the time remaining in the current window, and never 0 or fractional. Pinned in Task 2.
  5. Unbounded memory growth: entries for IPs that stop sending requests are evicted after their window expires. Pinned in Task 3.

Task 1: Configuration module

Files:

  • Create: src/config.js
  • Test: tests/config.test.js

Interfaces:

  • Consumes: process.env.RATE_LIMIT_MAX and process.env.RATE_LIMIT_WINDOW_MS.

  • Produces: getRateLimitConfig(env = process.env): { max: number, windowMs: number }. It defaults to { max: 100, windowMs: 60000 }.

  • [ ] Step 1: Write the failing tests in tests/config.test.js:

    • defaults to 100 requests per 60000ms: getRateLimitConfig({}) returns { max: 100, windowMs: 60000 }.
    • reads RATE_LIMIT_MAX and RATE_LIMIT_WINDOW_MS: getRateLimitConfig({ RATE_LIMIT_MAX: '5', RATE_LIMIT_WINDOW_MS: '1000' }) returns { max: 5, windowMs: 1000 }.
    • falls back to defaults for invalid values: for each of 'abc', '0', '-3', '' and '1.5', both variables fall back to their defaults.
  • [ ] Step 2: Run npx jest tests/config.test.js. Expected: FAIL with "Cannot find module '../src/config'".

  • [ ] Step 3: Implement getRateLimitConfig(env = process.env) in src/config.js. A value counts as valid only if it parses to a positive integer. Anything else falls back to the default.

  • [ ] Step 4: Run npx jest tests/config.test.js. Expected: PASS.

  • [ ] Step 5: Commit

    git add src/config.js tests/config.test.js
    git commit -m "feat: add rate limit config from env"
    

Task 2: Rate limiter middleware

Files:

  • Create: src/rateLimiter.js
  • Test: tests/rateLimiter.test.js

Interfaces:

  • Consumes: { max, windowMs } as produced by getRateLimitConfig (Task 1).

  • Produces: createRateLimiter({ max: number, windowMs: number, now?: () => number }): express.RequestHandler. The now option defaults to Date.now and exists so tests can control time. The middleware keys on req.ip. It calls next() while the count is at or below max. Otherwise it sets Retry-After to Math.ceil(msRemaining / 1000) and responds 429 with { error: 'Too Many Requests' }.

  • [ ] Step 1: Write the failing tests in tests/rateLimiter.test.js. Use a bare Express app with createRateLimiter({ max: 3, windowMs: 1000, now }), a GET /ping route, and supertest. now is a fake clock variable the test advances.

    • allows requests up to the limit: 3 requests return 200.
    • returns 429 with Retry-After once the limit is exceeded: the 4th request returns 429, and Number(res.headers['retry-after']) is an integer ≥ 1 and ≤ 1.
    • Retry-After reflects time remaining in window: with the clock advanced 400ms, the 429 carries Retry-After: 1. This is ceil(0.6), so it is never 0.
    • resets after the window elapses: advance the clock by 1000ms, and the next request returns 200.
    • tracks IPs independently: exhaust the limit with X-Forwarded-For: 1.1.1.1 (the app sets trust proxy in the test), and a request from 2.2.2.2 still returns 200.
  • [ ] Step 2: Run npx jest tests/rateLimiter.test.js. Expected: FAIL with "Cannot find module '../src/rateLimiter'".

  • [ ] Step 3: Implement createRateLimiter in src/rateLimiter.js. Use a closure-scoped Map<ip, { count, resetAt }> with a fixed window. A new window starts when now() >= resetAt.

  • [ ] Step 4: Run npx jest tests/rateLimiter.test.js. Expected: PASS.

  • [ ] Step 5: Commit

    git add src/rateLimiter.js tests/rateLimiter.test.js
    git commit -m "feat: add in-memory per-IP rate limiter middleware"
    

Task 3: Expired-entry eviction

Files:

  • Modify: src/rateLimiter.js
  • Test: tests/rateLimiter.test.js

Interfaces:

  • Consumes: createRateLimiter from Task 2.

  • Produces: the returned middleware gains a middleware.size(): number method that returns the number of tracked IPs. Expired entries are swept by an interval timer that is unref()'d. The middleware also gets middleware.stop(): void, which clears the timer so Jest can exit cleanly.

  • [ ] Step 1: Write the failing test evicts expired entries. Make requests from 3 IPs and assert size() is 3. Advance the fake clock past windowMs, trigger a sweep, and assert size() is 0. Use jest.useFakeTimers() to run the sweep interval, and call stop() in afterEach.

  • [ ] Step 2: Run npx jest tests/rateLimiter.test.js -t "evicts expired". Expected: FAIL with "size is not a function".

  • [ ] Step 3: Add the sweep to createRateLimiter. Use setInterval(sweep, windowMs).unref(), where sweep deletes entries whose resetAt <= now(). Attach size and stop to the returned handler.

  • [ ] Step 4: Run npx jest tests/rateLimiter.test.js. Expected: all PASS.

  • [ ] Step 5: Commit

    git add src/rateLimiter.js tests/rateLimiter.test.js
    git commit -m "feat: evict expired rate limit entries"
    

Task 4: Wire into the app

Files:

  • Modify: src/app.js, and package.json if supertest is missing (npm i -D supertest).
  • Test: tests/app.rateLimit.test.js

Interfaces:

  • Consumes: getRateLimitConfig (Task 1) and createRateLimiter (Task 2).

  • Produces: src/app.js mounts app.use(createRateLimiter(getRateLimitConfig())) before all routes. Before editing, check how src/app.js exports the app (the app itself, or a factory). Read the env at app-creation time so tests can set it.

  • [ ] Step 1: Write the failing test in tests/app.rateLimit.test.js. Set process.env.RATE_LIMIT_MAX = '2' and RATE_LIMIT_WINDOW_MS = '60000' before requiring the app, using jest.resetModules() if needed. Hit any existing route using the real app via supertest.

    • real app returns 429 with Retry-After after exceeding configured limit: two requests are not 429, the third returns 429, and the retry-after header is present.
  • [ ] Step 2: Run npx jest tests/app.rateLimit.test.js. Expected: FAIL because the third request returns 200.

  • [ ] Step 3: Mount the limiter in src/app.js as the first middleware, before any routes.

  • [ ] Step 4: Run the full suite with npx jest. Expected: all PASS, including the existing tests. If an existing test makes more than 100 requests from one IP, set RATE_LIMIT_MAX higher in that test's setup.

  • [ ] Step 5: Document RATE_LIMIT_MAX (default 100) and RATE_LIMIT_WINDOW_MS (default 60000) in the README, or in a new .env.example if no README exists.

  • [ ] Step 6: Commit

    git add src/app.js tests/app.rateLimit.test.js package.json README.md
    git commit -m "feat: apply rate limiting to API"
    

Self-Review

  • Spec coverage: the 100/min per-IP limit and the 429 with Retry-After are in Tasks 2 and 4. Env configuration is in Task 1. The in-memory store is in Tasks 2 and 3. Jest tests are in every task.
  • Step scan: each step names its file, signature and assertions, with no bodies written out.
  • Type consistency: { max, windowMs } and createRateLimiter are used identically across Tasks 1 to 4. size() and stop() are defined only in Task 3.
  • Review Focus: each of the five items has an owning test, in Tasks 1, 2 and 3.
  • Open assumption: Task 4 depends on how src/app.js exports the app. I haven't read that file, so the implementer should check it first.

Plan complete, and it is printed above rather than saved because I can't write files here. Please review it. Which execution approach would you prefer?

  • Subagent-driven: a fresh subagent implements each task and a fresh reviewer checks it before the next one starts. This is the most thorough option.
  • Native: I implement every task myself in this session, then one fresh reviewer checks the whole branch. This is the cheapest and fastest option.

For this plan I recommend Native. There are only four small tasks, and the interfaces are narrow and fully specified. A mistake in an in-memory limiter is cheap to catch and fix. Does the plan capture what you want, and which approach should we use?

Real run in an isolated folder with only this skill installed. Only the Skill and Read tools were enabled, so no command was run and no file was written. The example is fictional. The model could not save the plan to a file and printed it inline. The spec is invented.

Was es macht

Schreibt einen Plan für einen Entwickler, der weder die Codebasis noch die Spezifikation kennt. Der Plan hält die Entscheidungen fest, die der Umsetzer nicht allein treffen kann: welche Dateien, welche Namen und Signaturen, welche Werte aus der Spezifikation und welcher Test jede Aufgabe belegt. Er beginnt mit einem Kopf (Ziel, Architektur, Tech-Stack, die umgesetzte Spezifikation, globale Randbedingungen und eine Liste „Review-Schwerpunkte“ mit Eingaben, die die Spezifikation nahelegt, die aber kein Test abdeckt). Jede Aufgabe nennt die zu erstellenden oder zu ändernden Dateien, die genutzten und bereitgestellten Schnittstellen und kleine Test-zuerst-Schritte (fehlschlagenden Test schreiben, ausführen, implementieren, ausführen, committen) mit Befehl und erwarteter Ausgabe. Eine Selbstprüfung kontrolliert Spezifikationsabdeckung, Klarheit der Schritte, Typkonsistenz, Review-Schwerpunkte und Verhältnismäßigkeit, und der Plan endet mit der Bitte, ihn zu prüfen und die Ausführungsart zu wählen.

Geeignet für

Mehrstufige Funktionen mit schriftlicher Spezifikation, Übergabe an eine andere Person oder einen Agenten und kleine, testbare Aufgaben.

Hinweise & Risiken

Mittleres Risiko: Es speichert den Plan als `docs/superpowers/plans/<Datum>-<Funktion>.md` in Ihrem Projekt und legt dort also eine Datei an. Die geschriebenen Pläne enthalten `git commit`-Schritte und weisen den Ausführenden an, die Skills `subagent-driven-development` oder `executing-plans` zu nutzen, die nicht Teil dieser Listung sind; Sie brauchen also einen anderen Weg, den Plan umzusetzen. Es stellt keine Netzwerkanfragen und führt selbst keine Befehle aus. Einmal mit einer erfundenen Spezifikation ausprobiert; das Modell konnte die Datei nicht schreiben und gab den Plan stattdessen aus.