feat(api-effect): Effect v4/Bun mirror of the api - #54
Merged
Conversation
New lab-stack Mirror (CONTEXT-MAP): api-elysia's 9 route groups ported to effect@4.0.0-rc.112 + @effect/platform-bun HttpRouter, pinned exactly per ADR-0001. Shared Prisma 7 schema, byte-parity responses/cookies/Nest-shaped errors, ported bindBody validation and bun test suite (39 tests). Effect v4 wiring: HttpRouter.add layers per route, route-pattern-keyed CSRF+throttle guards, wildcard-unguarded 404 catch-alls (serve middleware cannot rewrite responses), Config for PORT/CORS_ORIGIN, CORS predicate accepting comma-lists and '*'. Verified: tsc clean, oxlint clean (only pre-existing copied-lib warnings), 39/39 tests, live curl matrix against shared-schema Postgres on :5439.
…ckage Per rules/libraries.md #11: import { cn } from 'cn' directly, no lib/utils re-export indirection. Delete both twMerge(clsx()) wrappers, rewrite 45 cn imports, drop clsx/tailwind-merge deps, point components.json utils alias at cn.
bun remove re-resolved @tiptap/core to 3.30.6 while @tiptap/react stayed at 3.23.4, splitting peer types and failing apps/web build. Update react so the whole graph resolves one version; net -140 lock lines of duplicates.
…fig-driven Db, scheduled sweep Framework pass, not a behavior change: parity matrix unchanged (400/401/403/404/409/501, cookie attrs, CORS echo/reject, 39/39 tests). - HttpApi + HttpApiBuilder: all 52 endpoints declared in src/api.ts, handlers via builder groups; raw HttpServerResponse is the seam for cookies/status (success: Schema.Unknown — validation stays bindBody for byte parity with the other mirrors). - Db is a Context.Service (Layer.effect + acquireRelease, DATABASE_URL via Config) provided to handlers through HttpRouter.provideRequest; routes use ctx.db. - Sweep moved from setInterval to an Effect scheduled task (Schedule.spaced) that forks inside the app layer's scope and dies with the server; logs via Logger. - CORS as chain middleware on serve/toWebHandler (same preflight-204 semantics). - Deliberate keeps, per the mirror's library decisions: bindBody validation, ioredis throttle, PayOS fetch, bun test (repo standards override the Effect-native option). - Two documented casts where rc-HttpApi's Request-marker requirements leak past toWebHandler/runPromise R constraints.
…for PayOS, boot-time Config validation - lib/redis.ts: ioredis dependency removed; getRedis() now fronts BunRedis (an Effect service) via ManagedRuntime with an ioredis-shaped adapter, the same 2s ping + degrade-to-null, and interrupt-driven cleanup on failure. Verified live: throttle 429 on the 11th strict-path request with Redis, all-200 degrade when Redis is unreachable. - lib/payos.ts: raw fetch -> HttpClient with FetchHttpClient.layer. - index.ts: DATABASE_URL + JWT_SECRET_KEY validated via Config.redacted at boot (fail fast before serving).
…dy, per-endpoint success schemas, zero route assertions
Route handlers are now proven-typed end to end:
- bindBody infers its result type from the field table (const generics + a
JsonKey/FieldValue mapped type): required:true fields are non-null strings,
enums come through as literals, optionals as T|null|undefined. All explicit
<T> assertions and `in1.x!` non-nulls are gone from every route file.
- Every endpoint declares a success schema mirroring its exact response
(nullable keys, permissive enums); wrap<A> checks each handler's return
value against it at .handle time. Custom status/cookie responses render
raw but are still type-checked as A.
- Path params via checked ctx.param() (no `!`); group endpoint declarations
now live beside their handlers (routes/*.ts export <g>Group + a local
single-group api for builders); api.ts just aggregates the groups.
- Enum casts (ProductSlug/EventType) replaced with type guards where the
value is validated first.
- Real bug caught by the typing pass: create-link declared the field as
OrderID, whose jsonKey('orderID') never matched what web sends
('orderId', per the Go struct tag). Renamed to OrderId.
- Remaining casts are bounded and intentional: validate.ts (the validation
boundary itself), copied blueprint libs (Prisma enum query filters),
prisma-fake test doubles, Array.includes widenings, and the one documented
rc-HttpApi Request-marker cast in index.ts.
39/39 tests; live E2E: register 201 {userId}, unknown-key 400 Go message,
empty-body 400 message array, email-verify 400, 404/401/403/CORS parity.
…+ live auth E2E
- HttpApiSwagger.layer(api, { path: '/docs' }) pays out the DTO work: full
OpenAPI 3.1.0 served inline (verified /v1 paths + {slug} params present).
Mounted OUTSIDE HttpRouter.provideRequest — that wrapper only relieves
Request-marker requirements and swallows plain-HttpRouter layers (rc quirk).
- validate.ts: shared isRecord predicate retires the JSON-object casts;
assertValidStatus is now a TS assertion so listOrders/listAllOrders/
adminUpdateStatus flow enum-narrowed values into Prisma with no casts.
- payos.ts: response handling narrows through isRecord/typeof — no casts.
- New lib/slugs.ts: single VALID_SLUGS + isProductSlug guard, shared by
products/reviews (was two local copies).
- Remaining casts are the documented boundary set: prisma-fake test doubles,
unvalidated enum pass-throughs to the DB (ProductSize/productSlug — matches
Go's reject-at-DB behavior), validate.ts raw->BindResult proof, and the one
rc-HttpApi marker cast.
- Verified live end-to-end against real Postgres+Redis: register 201 ->
email-verify -> login (2 Set-Cookie) -> /users/me (codec, goTime format) ->
sessions (device rows) -> refresh (rotation + re-set cookie) -> logout
(2 cleared cookies); 2fa-without-CSRF correctly 403s.
tsc 0 errors, oxlint clean, 39/39 tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
New lab-stack Mirror (
apps/api-effect): the 9 route groups ofapi-elysiare-implemented oneffect@4.0.0-rc.112+@effect/platform-bunHttpRouter. Shares the Prisma 7 multi-file schema; serves no real traffic (see rootCONTEXT.md→ Mirror, Lab stack).Design (grilling session, all decisions confirmed)
@effect/platform-bunon Bun, dev port 4003,bun --watchloop; no Docker/compose/nginx (Mirrors run locally).bindBodyvalidation (not Effect Schema), same lib deps.Configfor PORT/CORS_ORIGIN; CORS predicate accepting single origin / comma-list /*.4.0.0-rc.112pins, no^— recorded inapps/api-effect/docs/adr/0001-pin-effect-4-rc.md.Effect v4 wiring notes
HttpRouter.addis itself the route Layer; scope held withEffect.never.onBeforeHandle).Verification
bun test ./srcin app: 39/39; pre-commit hook ran the full turbo matrix green (8/8 tasks, all mirrors + prod api + infra).Dev loop