Tinker AI
Read reviews

Outcome

Migration completed in 4 days; ~3,200 lines changed across 28 files; zero production incidents post-deploy

9 min read

We had a Go service that started in 2019 with raw net/http for routing. It was small at the time. Three years and a lot of growth later, the routing layer was a 600-line switch statement plus a ladder of if r.Method == ... checks. It worked. It was painful to extend.

The decision: migrate to go-chi, which has clean routing, middleware support, and is the de-facto standard for Go HTTP services. The estimate without help: 6-8 days. With Aider, it took 4 days. This is the diary.

The starting point

The service:

  • ~12k lines of Go
  • 28 files in the internal/handlers package
  • 600-line routes.go doing path matching with switch+regex
  • 47 distinct route handlers
  • ~75% test coverage
  • Production traffic: ~200 RPS at peak, 5 9s availability target

The migration plan:

  • Replace routes.go with chi.Router setup
  • Convert each handler from (w, r) style to chi’s (w, r) (which is the same shape but uses chi.URLParam)
  • Migrate URL param extraction from regex captures to chi
  • Preserve all existing middleware (auth, logging, request ID)
  • No public API changes (paths and methods stay identical)
  • Keep tests passing throughout

Day 1: Setup and the routing skeleton

Started with Aider configured for Claude 3.5 Sonnet, with CONVENTIONS.md loaded and auto-commits: true. The conventions file already covered our Go style; nothing migration-specific.

First task: introduce go-chi as a dependency and stand up a parallel router with no routes yet. Make sure the build still works.

> /add go.mod go.sum cmd/server/main.go

> Add github.com/go-chi/chi/v5 as a dependency. In main.go, create a chi.Router 
> alongside the existing http.ServeMux but don't wire it up yet. We'll move 
> routes incrementally.

Aider added the dependency, ran go mod tidy, and produced a clean main.go with both routers initialized. Auto-commit captured this as a small step.

Then I did a 10-route prototype to validate the pattern:

> /add internal/handlers/users.go internal/handlers/routes.go cmd/server/main.go

> Migrate the GET /users and GET /users/{id} routes to chi. Specifically:
> - In main.go, register these routes on the chi router
> - In handlers/users.go, change chi.URLParam usage where needed (these were 
>   doing regex extraction before)
> - Don't remove the old route registrations yet; we'll do that in a sweep 
>   at the end

Aider produced a clean diff: chi routes registered, handler updated to use chi.URLParam(r, "id") instead of the old regex approach, the old switch case still in place but unreachable.

I ran the tests. They passed. I wrote a quick integration test against both routers (curl GET /users and verify response) to confirm chi was actually serving the migrated routes. Both worked.

Day 1 total: about 5 hours including the dependency wiring, prototype, and validation. Aider did most of the typing; I did the design and review.

Day 2: Bulk migration

With the pattern proven, I ran sessions to migrate routes in batches of 5-8 at a time. The prompt template:

> /add internal/handlers/<file>.go cmd/server/main.go

> Migrate these routes from the http.ServeMux registrations in main.go to 
> chi router registrations:
> - GET /resource/{id}
> - POST /resource
> - DELETE /resource/{id}
> 
> The handler functions in <file>.go need updating to use chi.URLParam 
> instead of the regex-based path parsing in the helper at line 42.
> 
> Don't remove the old switch case yet. Each newly migrated route should:
> 1. Be registered on the chi router in main.go
> 2. Have its URL param extraction updated to chi.URLParam
> 3. Keep all existing middleware behavior (the chi router has the same 
>    auth and logging middleware applied at startup)

Aider handled batches of 5-8 routes in maybe 3-4 minutes each, with clean diffs. Each batch became a commit. Tests passed after each batch.

Two patterns Aider got wrong on the first try:

Pattern 1: Optional URL params. Some old routes had patterns like /users/{id}/posts?since={timestamp}. Aider initially treated since as a URL param, when it’s a query param. Fixed in 30 seconds with a follow-up prompt.

Pattern 2: Body decoding helpers. Some routes used a custom helper to decode request bodies. Aider initially refactored these inline in the handler; I preferred them in the helper. One round-trip to fix.

Day 2 total: about 6 hours, migrating 35 of the 47 routes. Most of the time was running tests after each batch and reviewing diffs.

Day 3: Edge cases and finishing migration

The remaining 12 routes were the harder ones. Patterns that needed more thought:

Routes with complex regex. Three routes used regex patterns that didn’t translate cleanly to chi’s path matching. For these, I refactored the handler to do the matching internally and registered a broader path with chi.

> /add internal/handlers/exports.go

> The /exports/{type}/{id}.{format} route uses a regex to extract type, id, 
> and format. chi can match /exports/{type}/{id_dot_format} but the dot 
> separator is awkward. Refactor this to:
> - Register /exports/{type}/{filename} in chi
> - In the handler, parse {filename} into id and format internally
> - Keep the existing parseFilename helper logic, just call it from the handler

Aider handled this cleanly. The solution wasn’t pure pattern-matching; it was structural change. Aider’s plan-first approach was useful here — it proposed the refactor, I confirmed, then it executed.

Middleware ordering quirks. Two routes had subtle middleware order requirements (auth must run before logging because the logger reads the user from context). I had to make this explicit:

> The middleware order on the chi router must be: requestID -> logging -> auth.
> The logging middleware reads user info from the request context, but only if 
> set. The auth middleware sets user info. So auth must run before logging in 
> request order, but middleware in chi is registered in stack order, so to get 
> auth-runs-first you register logging-then-auth (because chi middleware is 
> applied as .Use() outer-then-inner).
> 
> Verify this in main.go and fix if wrong.

Aider verified, found the order was wrong in one place, fixed it, and added a comment explaining the order requirement. This was a moment where the AI’s contribution was real — I’d have spent more time figuring out the ordering myself.

The websocket handler. One route was a websocket upgrade. chi handles this fine, but the existing code did some manual routing inside the handler that conflicted with chi’s expectations. Migrated by hand, not via Aider, because the conflict required understanding the websocket lifecycle and Aider was producing plausible but wrong output.

Day 3 total: about 7 hours. The 12 remaining routes plus the cleanup of the old switch statement.

Day 4: Removing the old code and verification

Final day was sweeping the old routing code:

> /add cmd/server/main.go internal/handlers/routes.go

> All routes are now migrated to chi. Remove the old http.ServeMux 
> registration and the routes.go switch statement entirely. The chi 
> router is now the only HTTP handler. Update main.go to register chi 
> as the http.Handler.

Aider produced a clean removal. Tests still passed. I ran the integration test suite (~80 tests against a running instance) and they all passed.

Then verification work that wasn’t really Aider-flavored:

  • Performance check: benchmark the chi-routed service vs. the old switch-routed service. chi was within 2% on routing latency, well within noise.
  • Memory check: heap size at idle and under load. Slightly lower with chi (chi has more efficient internal data structures than my switch statement).
  • Production deploy with feature flag: deployed to 1% of traffic for 2 hours, then 10%, then 50%, then 100%. No errors at any stage.

Day 4 total: about 5 hours including verification and the staged production deploy.

The metrics

MetricValue
Total time4 days (~23 hours)
Original estimate without AI6-8 days
Lines changed~3,200
Files touched28
Aider sessions~25
Aider auto-commits~80
Tokens spent~3.5M input, ~150k output
Aider API cost~$11 (paid via Anthropic API)
Production incidents post-deploy0

The savings: about 2-3 days off the estimate. Not a 10x speedup; a real one nonetheless. The 28 files would have been tedious to migrate by hand; Aider compressed the typing without compressing the design or verification.

What worked

Auto-commits per Aider response. Each route batch became a commit. The git history shows the migration as a sequence of small steps, each individually verifiable. When something went wrong, I could revert one commit without losing the others.

Plan-mode for harder cases. For the regex-handler refactor and the middleware ordering, asking Aider to plan first surfaced bad assumptions before tokens were spent on bad implementations.

Running tests after each commit. Catching breakage early kept the migration tractable. If I’d batched everything to the end, the inevitable errors would have been entangled and harder to diagnose.

The CONVENTIONS.md anchor. Aider stayed on style throughout. I didn’t have to fix import orders, naming conventions, or error-handling patterns in any of the diffs.

What didn’t work

The websocket handler in Aider. Migrating this required understanding state I couldn’t easily express to Aider. I switched to manual editing for this one piece. About 90 minutes of focused hand-coding.

Trying to skip the prototype. I almost started day 2 without doing the day 1 prototype. The prototype caught one wrong assumption (about how chi handles trailing slashes) early. Skipping it would have cost more than the prototype took.

Letting Aider remove the old code in one big sweep. I tried having Aider remove routes.go in pieces alongside the migration. This created unstable intermediate states. I switched to “leave the old code, migrate to chi, remove old code at the end” and that worked better.

What I’d do similarly next time

For Go service migrations with similar shape (changing a cross-cutting concern like routing, logging, or DI), the pattern was: prototype on a small slice → sweep through the bulk → handle edge cases by name → cleanup at the end. Each phase has Aider doing different fractions of the work.

The 4-day timeline is good but not exceptional. The bigger win was the cleanliness of the resulting diff — every file change was small and reviewable, the git history reads like a deliberate migration rather than a jumbled refactor, and the production deploy was uneventful because the pieces were verified incrementally.

For migrations specifically, Aider’s auto-commit + per-step verification model is uniquely well-suited compared to other AI tools. Composer-style multi-file diffs are too coarse for migration work; Cursor’s chat is more friction than helpful at this scale. Aider’s git-native rhythm is the right fit.