Practical notes for an eventual migration of packages/opencode server routes from the current Hono handlers to Effect HttpApi, either as a full replacement or as a parallel surface.
Use Effect HttpApi where it gives us a better typed contract for:
This should be treated as a later-stage HTTP boundary migration, not a prerequisite for ongoing service, route-handler, or schema work.
HttpApi is definition-first.
HttpApi is the root APIHttpApiGroup groups related endpointsHttpApiEndpoint defines a single route and its request / response schemasThis is a better fit once route inputs and outputs are already moving toward Effect Schema-first models.
The current route-effectification work is already pushing handlers toward:
AppRuntime.runPromise(Effect.gen(...)) bodyThat work is a good prerequisite for HttpApi. Once the handler body is already a composed Effect, the remaining migration is mostly about replacing the Hono route declaration and validator layer.
Request params, query, payload, success payloads, and typed error payloads are declared in one place using Effect Schema.
Incoming data is decoded through Effect Schema instead of hand-maintained Zod validators per route.
HttpApi can derive OpenAPI from the API definition, which overlaps with the current describeRoute(...) and resolver(...) pattern.
Schema.TaggedErrorClass maps naturally to endpoint error contracts.
Best fit first:
Harder / later fit:
Many route boundaries still use Zod-first validators. That does not block all experimentation, but full HttpApi adoption is easier after the domain and boundary types are more consistently Schema-first with .zod compatibility only where needed.
Many current server/instance/*.ts handlers still call async facades directly. Migrating those to composed Effect.gen(...) handlers is the low-risk step to do first.
The server currently includes SSE, websocket, and streaming-style endpoints. Those should not be the first HttpApi targets.
The current server composition, middleware, and docs flow are Hono-centered today. That suggests a parallel or incremental adoption plan is safer than a flag day rewrite.
server/instance/*.tsIntroduce one small HttpApi group for plain JSON endpoints only. Good initial candidates are the least stateful endpoints in:
server/instance/question.tsserver/instance/provider.tsserver/instance/permission.tsAvoid session.ts, SSE, websocket, and TUI-facing routes first.
Recommended first slice:
questionGET /questionPOST /question/:requestID/replyWhy question first:
Do not re-architect business logic during the HTTP migration. HttpApi handlers should call the same Effect services already used by the Hono handlers.
Prefer mounting an experimental HttpApi surface alongside the existing Hono routes first. That lowers migration risk and lets us compare:
If the parallel slice works well, migrate additional JSON route groups one at a time. Leave streaming-style endpoints on Hono until there is a clear reason to move them.
Every HttpApi slice should follow specs/effect/schema.md and the Schema -> Zod interop rule in specs/effect/migration.md.
Default rule:
.zod exists only as a compatibility surfacePractical implication for HttpApi migration:
@/util/effect-zodOrdering for a route-group migration:
schema.ts leaf types to Effect Schema firstInfo / Input / Output route DTOs to Effect SchemaSchema.TaggedErrorClass where needed.zodHttpApi contract from the canonical Effect schemasTemporary exception:
The first HttpApi spike should be intentionally small and repeatable.
Chosen slice:
questionGET /question and POST /question/:requestID/replyNon-goals:
session routesBehavior rule:
404 behavior as a separate follow-up unless they are required to make the contract honestAdd POST /question/:requestID/reject only after the first two endpoints work cleanly.
Use the same sequence for each route group.
HttpApi contract separately from the handlers.Rule of thumb:
Placement rule:
HttpApi code under src/server, not src/effectsrc/effect should stay focused on runtimes, layers, instance state, and shared Effect plumbingHttpApi slice next to the HTTP boundary it servessrc/server/instance/httpapi/*src/server/control/httpapi/*Suggested file layout for a repeatable spike:
src/server/instance/httpapi/question.tssrc/server/instance/httpapi/index.tstest/server/question-httpapi.test.tstest/server/question-httpapi-openapi.test.tsSuggested responsibilities:
question.ts defines the HttpApi contract and HttpApiBuilder.group(...) handlers for the experimental sliceindex.ts combines experimental HttpApi groups and exposes the mounted handler or layerquestion-httpapi.test.ts proves the route works end-to-end against the real servicequestion-httpapi-openapi.test.ts proves the generated OpenAPI is acceptable for the migrated endpointsEach route-group spike should follow the same shape.
HttpApiHttpApiGroupHttpApiBuilder.group(api, groupName, ...)/experimental/httpapiThe first slices should keep the existing outer server composition and only replace the route contract and handler layer.
AuthMiddleware at the outer Hono app levelHttpApi group for the first parallel slicesHttpApi handlerPractical rule:
HttpApi route should stay mounted behind that same stackWorkspaceRouterMiddleware as the source of truth for resolving directory, workspace, and session-derived workspace contextInstance.current and WorkspaceContext before the request reaches the HttpApi handlerHttpApi handlers unaware of path-to-instance lookup details when the existing Hono middleware already handles themPractical rule:
HttpApi handlers should yield services from context and assume the correct instance has already been providedHttpApi layer if we later decide to migrate the outer middleware boundary itselfErrorMiddleware for expected route behaviorPractical rule:
400sFor the current parallel slices, this means:
HttpApiHttpApiThe first slice is successful if:
.zod or clearly temporaryHttpApi contractThe first parallel question spike gave us a concrete pattern to reuse.
Schema.Class works well for route DTOs such as Question.Request, Question.Info, and Question.Reply.Question.Answer should stay as schemas and use helpers like withStatics(...) instead of being forced into classes.HttpApi success schema uses Schema.Class, the handler or underlying service needs to return real schema instances rather than plain objects.Status legend:
done - parallel HttpApi slice existsnext - good near-term candidatelater - possible, but not first wavedefer - not a good early HttpApi targetCurrent instance route inventory:
question - done
endpoints in slice: GET /question, POST /question/:requestID/replypermission - done
endpoints in slice: GET /permission, POST /permission/:requestID/replyprovider - next
best next endpoint: GET /provider/auth
later endpoint: GET /provider
defer first-wave OAuth mutationsconfig - next
best next endpoint: GET /config/providers
later endpoint: GET /config
defer PATCH /config for nowproject - later
best small reads: GET /project, GET /project/current
defer git-init mutation firstworkspace - later
best small reads: GET /experimental/workspace/adaptor, GET /experimental/workspace, GET /experimental/workspace/status
defer create/remove mutations firstfile - later
good JSON-only candidate set, but larger than the current first-wave slicesmcp - later
has JSON-only endpoints, but interactive OAuth/auth flows make it a worse early fitsession - defer
large, stateful, mixes CRUD with prompt/shell/command/share/revert flows and a streaming routeevent - defer
SSE onlyglobal - defer
mixed bag with SSE and process-level side effectspty - defer
websocket-heavy route surfacetui - defer
queue-style UI bridge, weak early HttpApi fitRecommended near-term sequence after the first spike:
provider auth read endpointconfig providers read endpointproject read endpointsworkspace read endpointsHttpApi group for a simple JSON route setHttpApi should stay parallel, replace only some groups, or become the long-term defaultDo not start with the hardest route file.
If HttpApi is adopted here, it should arrive after the handler body is already Effect-native and after the relevant request / response models have moved to Effect Schema.