effx
Coming from…

Coming from NestJS

A concept map from NestJS controllers, pipes, guards and filters to effx annotations and the Effect constructs underneath, a list of gaps, and one endpoint ported side by side.

effx bridges enterprise frameworks and full-stack Effect. You write a familiar declarative surface (decorators or builder chains). The compiler reads it ahead of time and writes ordinary Effect that you can read, edit or keep. This page maps NestJS concepts onto that surface and says, row by row, what exists today (ADR 0012).

Pinned versions. NestJS claims cite docs.nestjs.com. As fetched on 2026-10-04 that site documents NestJS 12 (its migration guide covers "version 11 to version 12"). Effect claims cite the official clone at https://github.com/Effect-TS/effect/tree/b5a2d4c1d62c9620a68d72b7f20248c69ef7663b (commit b5a2d4c, package effect 4.0.0). effx claims cite a spec or a source path in this repository.

Read this first: what a decorator means

NestJSeffx
A decorator such as @UseGuards or @GetMetadata the Nest runtime reads to route requests and run guards, pipes, interceptors and filters (Guards: they run "after all middleware, but before any interceptor or pipe").A contribution to the IR. Runtime decorators are no-ops or metadata only, and the compiler (not a runtime) reads the source. Source: AGENTS.md invariants 1 and 2; ADR 0001.
Dependency injectionA runtime container resolves constructor parameters (Providers).No container. A handler's requirements R are inferred, and @Requirements asserts them. You provide Layers yourself. Source: ADR 0005, ADR 0012 ("Rules out" hidden DI).
FailuresExceptions thrown anywhere, rendered by filters (Exception filters).Typed errors E in the handler's Effect<A, E, R>. @Errors asserts them and mismatches are diagnostics. Source: ADR 0005; apps/docs/content/docs/authoring/operations.mdx.
OutputBehaviour lives in the framework at runtime.Readable files in .effx/generated. A team can stop running the compiler and keep them. Source: ADR 0008, ADR 0012.

Status vocabulary. supported: effx implements it (cited source or spec). via ecosystem package: an Effect package does it and effx composes with it; the path is cited. gap: nothing in effx today (not called "planned" unless a spec says so). A row can carry two statuses: first what effx itself provides, then what the Effect ecosystem provides.

Concept map

HTTP surface

NestJS concepteffx annotation or constructUnderlying Effect constructStatusSource
Controllers and routing: @Controller, @Get, @Patch, ...@Http.Get/Post/Put/Patch/Delete(path), @Http.Group, @Http.Contract; builder .http.get(...), .in(group)HttpApiEndpoint, HttpApiGroup, HttpApi, HttpApiBuilder.groupsupportedSpec 0005; packages/runtime/src/decorators.ts; packages/compiler/src/generate/http.ts; real output examples/users/.effx/generated/http.ts. See HTTP.
DTO validation: ValidationPipe with class-validator, pipesinput and success on @Query/@Command; params, query, headers, payload on @Http.ContractSchema (Schema.Struct, Schema.NonEmptyString); HttpApiEndpoint decodes each channel before the handler runssupportedexamples/users/src/schemas.ts; spec 0005; packages/runtime/src/Annotation.ts (HttpContractOptions); Effect packages/effect/src/http-api/HttpApiBuilder.ts:866-880. See Http.Contract.
DI, modules, providers: providers, modules@Requirements(Service) asserts R; @Errors asserts E. No module or container.Context.Service, Layer (composed by the application)supported (assertion only; wiring is yours)ADR 0005; examples/users/src/services.ts; examples/users/src/server-main.ts (Layer.provide(Users.layer)); Effect packages/effect/src/Layer.ts, Context.ts.
Middleware: NestMiddlewaremiddleware: [Marker] on @Http.Contract, or defaults.middleware on a groupHttpApiMiddleware.Service marker class; app-wide HttpMiddleware (logger, tracer, cors, compression)supported (attaching the marker); via ecosystem package (the implementation)Spec 0005, spec 0013; apps/docs/content/docs/authoring/http-contract.mdx ("Middleware and security markers"); Effect packages/effect/src/http-api/HttpApiMiddleware.ts, packages/effect/src/http/HttpMiddleware.ts. effx never instantiates the marker.
Interceptors: NestInterceptorNo annotation.HttpApiMiddleware wraps endpoint execution; Effect combinators (Effect.withSpan, Effect.tap) inside handlers; the endpoint success schema shapes the responsegap (effx); via ecosystem package (Effect)packages/effect/src/http-api/HttpApiMiddleware.ts (module header); packages/effect/src/Effect.ts (withSpan).

Security and errors

NestJS concepteffx annotation or constructUnderlying Effect constructStatusSource
Guards: CanActivate, @UseGuards@Http.Access({...}) records the access contract. The generator requires a typed guard in a generated Guards record, passed to the handler as a lazy authorize() thunk.endpoint.annotateMerge(annotator(spec)) (a Context you supply); HttpApiMiddleware with HttpApiSecurity as the security markersupported (declaration and typed binding); evaluation is application codeSpec 0006; packages/compiler/src/generate/guards.ts; packages/compiler/src/generate/http.ts (handlerLine). See Problems and access.
Filters and exception handling: @Catch, ExceptionFilterTyped errors in the handler; @Errors(...) asserts them; a literal httpApiStatus on an exported error class is recognized as its HTTP statusSchema.TaggedError with { httpApiStatus: N }; the endpoint error option; Effect.catch* in handlerssupportedADR 0005; packages/frontend-ts/src/signature.ts:46-85 (literalHttpStatus); packages/compiler/src/extensions/problem-contract.ts. An error with no status is encoded as 500 (Effect packages/effect/src/http-api/HttpApiSchema.ts:1099-1100).
Auth and authorization policies: authentication, authorization (@Policy(), @Can())@Authorize(Capability.make(...)) records capability and focus facts; @Http.Access records credentials, principals, capabilities and scope. Nothing evaluates them.Credential schemes: HttpApiSecurity (bearer, apiKey, basic) inside an HttpApiMiddleware.Service. No policy engine found in the Effect clone.supported (declaration only); gap (authorization runtime: Cedar and leases are deferred); via ecosystem package (credential extraction)ADR 0007; spec 0006; apps/docs/content/docs/authoring/persistence-and-authority.mdx; Effect packages/effect/src/http-api/HttpApiSecurity.ts. The current NestJS authorization chapter documents @nestjs/authorization policies; it does not mention CASL (searched 2026-10-04).
Problem-details style error responses (a custom filter; on the Exception filters page I found statusCode/message bodies and errorCode, and no Problem Details section)@Http.Problems({ registry, codes, map }) and your ProblemRegistry functionEndpoint error schemas derived by your registry; HttpApiSchema statussupported (contract); the domain-error to Problem conversion is hand-writtenSpec 0005 §"Problem responses"; packages/compiler/src/extensions/problem-contract.ts; packages/compiler/src/generate/http.ts (problemErrors). See the warning in Problems and access.

Contracts and tooling

NestJS concepteffx annotation or constructUnderlying Effect constructStatusSource
OpenAPI / Swagger: @nestjs/swaggermetadata (operationId, summary, description, tags, annotator) on @Http.Contract; group title, description, displayNameOpenApi.annotations; the document comes from OpenApi.fromApi; UI from HttpApiSwagger or HttpApiScalarsupported (annotations); via ecosystem package (document and UI; effx wires no UI)Spec 0005, spec 0010; packages/frontend-ts/test/fixtures/stable-v4/src/profile-openapi.spec.ts (real OpenApi.fromApi); STATE.md Gate 3 evidence; Effect packages/effect/src/http-api/OpenApi.ts, HttpApiSwagger.ts, HttpApiScalar.ts.
Client generation. I found no first-party client generator in NestJS's OpenAPI chapter; its types chapter refers to tools that take the spec as input and generate client code.Generated client.ts (typed client plus operation index); opt-in Foldkit CommandHttpApiClientsupportedSpec 0007; packages/compiler/src/generate/client.ts; real output examples/users/.effx/generated/client.ts.
Configuration: ConfigModule, ConfigServiceNo annotation.Config, ConfigProvidergap (effx); via ecosystem package (Effect)Real use: examples/users/src/server-main.ts (Config.Number("PORT").pipe(Config.withDefault(3000))); Effect packages/effect/src/Config.ts, ConfigProvider.ts.
CLI and scaffolding: nest generate@Cli("users get") projects an operation to a CLI command. The effx CLI has check, build, inspect, graph. No generator command.effect/cli Commandsupported (CLI projection); gap (scaffolding)Spec 0003; scripts/effx.ts; packages/compiler/src/generate/cli.ts; real output examples/users/.effx/generated/cli.ts.

Infrastructure

NestJS concepteffx annotation or constructUnderlying Effect constructStatusSource
ORM and migrations: TypeORM, Prisma, MikroORM@PersistentModel records model, views and focus metadata. No SQL, repository or migration is generated.SqlClient, SqlSchema, SqlModel, Migrator (package effect, effect/sql); drivers such as @effect/sql-pggap (effx); via ecosystem package (Effect)Spec 0008 is "proposed, not frozen"; STATE.md lists persistence as parked; examples/users uses an in-memory Ref. Effect packages/effect/src/sql/{SqlClient,SqlSchema,SqlModel,Migrator}.ts, packages/sql/pg.
Jobs, queues, scheduling: Queues (Bull/BullMQ), @nestjs/scheduleNo annotation.Schedule, Cron, Queue; durable: PersistedQueue (memory, Redis or SQL store), DurableQueue, ClusterCrongap (effx); via ecosystem package (Effect)Effect packages/effect/src/{Schedule,Cron,Queue}.ts, persistence/PersistedQueue.ts, workflow/DurableQueue.ts, cluster/ClusterCron.ts. Parity with BullMQ was not evaluated.
Caching: CacheInterceptor, @CacheKeyNo annotation.Cache, ScopedCache; durable: PersistedCachegap (effx); via ecosystem package (Effect)Effect packages/effect/src/Cache.ts, ScopedCache.ts, persistence/PersistedCache.ts. There is no declarative per-route cache.
Realtime: WebSocket gateways, SSENo annotation. The generated RPC server uses protocol: "http".Socket; RPC over WebSocket (RpcServer.layerHttp with protocol: "websocket"); Sse encodinggap (effx); via ecosystem package (Effect)Real output examples/users/.effx/generated/http.ts (last line); Effect packages/effect/src/socket/Socket.ts, rpc/RpcServer.ts:872-895, encoding/Sse.ts.
Testing: Test.createTestingModuleNo harness.Layer substitution; @effect/vitest; TestClock; HttpApiTestgap (effx); via ecosystem package (Effect)effx's own pattern: examples/users/test/e2e.test.ts (generated routes, clients and Users.layer on BunHttpServer.layerTest). Effect packages/vitest, packages/effect/src/testing/TestClock.ts, http-api/HttpApiTest.ts, ai-docs/src/51_http-server/20_testing.ts.
Observability: logging, health checks, observabilityNo annotation. Generated code adds no spans or logs.Logger, Tracer, Effect.withSpan, HttpMiddleware.logger/tracer, @effect/opentelemetry, effect/observability (OTLP, Prometheus). A health check is an ordinary endpoint.gap (effx); via ecosystem package (Effect)Effect packages/effect/src/{Logger,Tracer}.ts, http/HttpMiddleware.ts:149,199, observability/, packages/opentelemetry; ai-docs/src/08_observability/; ai-docs/src/51_http-server/fixtures/api/System.ts (health endpoint). I found no Terminus-style indicator module in the clone.

Gaps today

effx has no declaration or generator for the items below. Where an Effect package exists, you use it directly in handlers and layers, and it coexists with generated code (ADR 0012: "Raw Effect always coexists").

  • Authorization runtime. @Authorize and @Http.Access record facts. Cedar decisions and leases are deferred (ADR 0007). The compiler checks that a guard binding is present, not that the handler calls it (spec 0006). Enforcement evidence exists only in mono-web's unlanded browser and PostgreSQL journeys (STATE.md).
  • Domain error to Problem conversion. The generated handler is an ordinary Effect handler. The conversion is hand-written until a runtime mapping contract exists (spec 0005).
  • Persistence. Spec 0008 is proposed, not frozen. @PersistentModel records metadata only.
  • Jobs, queues and scheduling. No annotation, no generator, no spec.
  • Caching. No annotation and no per-route cache declaration.
  • Realtime. No WebSocket or SSE annotation. Generated RPC is HTTP only.
  • Configuration, logging, tracing, health checks. No annotation. Use Config, Logger, Tracer and ordinary endpoints.
  • Testing harness. No module-override equivalent. Tests provide Layers directly.
  • Scaffolding. No nest g equivalent. The CLI has check, build, inspect and graph.
  • Global interceptors and filters. There is no app-wide declaration. Attach a marker with Http.Contract.middleware or a group's defaults.middleware, or use Effect HttpMiddleware.
  • Query is a claim, not a proof. @Query does not stop a handler from writing. The analysis that would flag it is not implemented (ADR 0006).
  • Distribution. The packages are not published to a registry. You run effx from the monorepo (Getting started).

Port: PATCH /users/:id/email

The endpoint under port:

  • PATCH /users/:id/email with body { "email": "..." }.
  • Only the signed-in user may change their own email (a guard).
  • The email must be non-empty (validation).
  • Problem-style responses: 404 when the user does not exist, 409 when the email belongs to another user.

NestJS code cites the pages named in each block. The effx tab uses the real files under examples/users/src where they match, and labelled pasted snippets where it extends them.

DTO. Rules are decorators on a class. A global ValidationPipe enforces them.

users/change-email.dto.ts
import { IsNotEmpty } from 'class-validator';

export class ChangeEmailDto {
  @IsNotEmpty()
  email: string;
}
main.ts (excerpt)
app.useGlobalPipes(new ValidationPipe());

Source: Validation (IsNotEmpty, "Auto-validation").

Domain exception. A custom exception extends HttpException, so Nest renders it.

users/user-problem.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';

export class UserProblem extends HttpException {
  constructor(
    readonly code: string,
    title: string,
    status: HttpStatus,
  ) {
    super(title, status);
  }
}

Source: Exception filters ("Custom exceptions").

Guard. Returns true to proceed. Returning false makes Nest throw ForbiddenException.

users/owner.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';

@Injectable()
export class OwnerGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    // verifyToken stands for your authentication, as validateRequest() does in the Guards chapter.
    const caller = verifyToken(request.headers.authorization);

    return caller !== undefined && caller.userId === request.params.id;
  }
}

Source: Guards ("Authorization guard", "When a guard returns false...").

Exception filter. Renders a problem-style body for every HttpException, including the guard's 403 and the pipe's 400.

users/problem.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
import { UserProblem } from './user-problem.exception';

@Catch(HttpException)
export class ProblemFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response.status(status).json({
      code: exception instanceof UserProblem ? exception.code : undefined,
      title: exception.message,
      status,
      instance: request.url,
    });
  }
}

Source: Exception filters ("Exception filters", "Binding filters").

Controller and service.

users/users.controller.ts
import { Body, Controller, Param, Patch, UseFilters, UseGuards } from '@nestjs/common';

@Controller('users')
@UseFilters(ProblemFilter)
export class UsersController {
  constructor(private readonly users: UsersService) {}

  @Patch(':id/email')
  @UseGuards(OwnerGuard)
  changeEmail(@Param('id') id: string, @Body() dto: ChangeEmailDto) {
    return this.users.setEmail(id, dto.email);
  }
}
users/users.service.ts
@Injectable()
export class UsersService {
  private readonly users = new Map<string, { id: string; email: string; displayName: string }>([
    ['1', { id: '1', email: 'alice@example.com', displayName: 'Alice' }],
    ['2', { id: '2', email: 'bob@example.com', displayName: 'Bob' }],
  ]);

  setEmail(id: string, email: string) {
    const user = this.users.get(id);

    if (user === undefined) {
      throw new UserProblem('user.not-found', 'User not found', HttpStatus.NOT_FOUND);
    }

    for (const [otherId, other] of this.users) {
      if (otherId !== id && other.email === email) {
        throw new UserProblem('user.email-taken', 'Email already in use', HttpStatus.CONFLICT);
      }
    }

    const updated = { ...user, email };
    this.users.set(id, updated);

    return updated;
  }
}

Source: Controllers (@Controller, @Patch, @Param, @Body; PATCH returns 200 by default), Providers (@Injectable, constructor injection), Modules (list UsersController and UsersService in an @Module). The service mirrors examples/users/src/services.ts.

What the port preserves

BehaviourNestJSeffxEvidence
Route and method@Patch(':id/email')@Http.Patch("/users/:id/email")examples/users/src/operations.ts; generated http.ts
Non-empty email rejected@IsNotEmpty() through ValidationPipeSchema.NonEmptyString, decoded before the handlerexamples/users/src/schemas.ts; HttpApiBuilder.ts:866-880
404 and 409UserProblem with NOT_FOUND and CONFLICTTyped errors mapped to codes by Http.Problems, status from httpApiStatusproblem-contract.ts; signature.ts:46-85
Success status200 (the default for PATCH)User.Self success, no status set, so Effect's default of 200 applies (HttpApiSchema.ts:1072-1073; not observed for this endpoint)Nest Controllers; packages/effect/src/http-api/HttpApiSchema.ts
Only the owner may change the emailOwnerGuard returns false, Nest renders 403Http.Access declares the contract; your guard and handler decide; AccessDenied is a typed 403Spec 0006

What it does not preserve

  • effx declares an access contract. It does not evaluate it. Http.Access is data (credentials, principal kinds, capability, requirements, scope resolver, decision time). Your guard record decides, and your handler chooses when to call authorize(). A typed binding is not proof that the handler called it (spec 0006). The ownership check above is application code.
  • Order differs. In Nest a guard runs before pipes (Guards), so a non-owner with an invalid body gets 403. In effx the request is decoded before the handler runs (HttpApiBuilder.ts:866-883) and authorize() runs inside the handler, so the same request gets the decode failure first. Credential rejection by the UserSecurity marker's Layer is a separate step, and its order relative to decoding was not examined.
  • Validation errors are not problem-style by default. Nest's ValidationPipe returns 400 with messages (Validation), and the filter above reshapes it. A decode failure in Effect HttpApi is an HttpApiSchemaError, which responds as an empty 400 (packages/effect/src/http-api/HttpApiError.ts:470-471). You can map it with HttpApiMiddleware.layerSchemaErrorTransform (HttpApiMiddleware.ts:489). effx generates no such mapping.
  • No catch-all filter. The NestJS filter shapes every HttpException. In effx, only errors declared on an operation have a response schema. An error outside E is a defect.
  • The problem body is whatever your registry returns. Here it is the encoded UserNotFound or EmailTaken (a _tag and its fields), not a Problem Details document. A registry can return { code, title } schemas instead (ai-docs/src/04_problems-and-access/fixtures/settings.ts), but then converting the domain error to that body is hand-written (spec 0005).
  • The service is Effect, not a provider. Users is a Context.Service with an in-memory Layer (examples/users/src/services.ts), not a persistence layer. UsersService in the NestJS tab is in-memory too, so the comparison covers the endpoint, not storage (spec 0008 is proposed).
  • The authorize thunk is a handler parameter. In NestJS the guard never appears in the handler's signature. In effx it does, which is how the requirement stays visible in types.
  • Generated code stays readable. The two generated excerpts above are real compiler output, ordinary HttpApi code. See Generated code.

Where to go next

On this page