Effect by Example: Next.js API Handler (App Router)

Tags:

import {
import HttpApp
HttpApp
,
import HttpServerRequest
HttpServerRequest
,
import HttpServerResponse
HttpServerResponse
,
import UrlParams
UrlParams
,
} from "@effect/platform";
import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
,
import Layer
Layer
,
import ManagedRuntime
ManagedRuntime
} from "effect";
// your main layer representing all of the services your handler needs (db, auth, etc.)
declare const
const mainLive: Layer.Layer<void, never, never>
mainLive
:
import Layer
Layer
.
interface Layer<in ROut, out E = never, out RIn = never>

@since2.0.0

@since2.0.0

Layer
<void>;
const
const managedRuntime: ManagedRuntime.ManagedRuntime<void, never>
managedRuntime
=
import ManagedRuntime
ManagedRuntime
.
const make: <void, never>(layer: Layer.Layer<void, never, never>, memoMap?: Layer.MemoMap | undefined) => ManagedRuntime.ManagedRuntime<void, never>

Convert a Layer into an ManagedRuntime, that can be used to run Effect's using your services.

@since2.0.0

@example

import { Console, Effect, Layer, ManagedRuntime } from "effect"
class Notifications extends Effect.Tag("Notifications")<
Notifications,
{ readonly notify: (message: string) => Effect.Effect<void> }
>() {
static Live = Layer.succeed(this, { notify: (message) => Console.log(message) })
}
async function main() {
const runtime = ManagedRuntime.make(Notifications.Live)
await runtime.runPromise(Notifications.notify("Hello, world!"))
await runtime.dispose()
}
main()

make
(
const mainLive: Layer.Layer<void, never, never>
mainLive
);
const
const runtime: Runtime<void>
runtime
= await
const managedRuntime: ManagedRuntime.ManagedRuntime<void, never>
managedRuntime
.
ManagedRuntime<void, never>.runtime: () => Promise<Runtime<void>>
runtime
();
// everything interesting happens in this effect- it consumes the request from context anywhere and ultimately produces some http response
declare const
const effectHandler: Effect.Effect<HttpServerResponse.HttpServerResponse, never, HttpServerRequest.HttpServerRequest>
effectHandler
:
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that describes a workflow or job, which can succeed or fail.

Details

The Effect interface represents a computation that can model a workflow involving various types of operations, such as synchronous, asynchronous, concurrent, and parallel interactions. It operates within a context of type R, and the result can either be a success with a value of type A or a failure with an error of type E. The Effect is designed to handle complex interactions with external resources, offering advanced features such as fiber-based concurrency, scheduling, interruption handling, and scalability. This makes it suitable for tasks that require fine-grained control over concurrency and error management.

To execute an Effect value, you need a Runtime, which provides the environment necessary to run and manage the computation.

@since2.0.0

@since2.0.0

Effect
<
import HttpServerResponse
HttpServerResponse
.
interface HttpServerResponse

@since1.0.0

HttpServerResponse
,
never,
import HttpServerRequest
HttpServerRequest
.
interface HttpServerRequest

@since1.0.0

@since1.0.0

HttpServerRequest
>;
// example:
declare const
const doThing: (id: string) => Effect.Effect<{
readonly _tag: "user";
}, Error>
doThing
: (
id: string
id
: string,
) =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that describes a workflow or job, which can succeed or fail.

Details

The Effect interface represents a computation that can model a workflow involving various types of operations, such as synchronous, asynchronous, concurrent, and parallel interactions. It operates within a context of type R, and the result can either be a success with a value of type A or a failure with an error of type E. The Effect is designed to handle complex interactions with external resources, offering advanced features such as fiber-based concurrency, scheduling, interruption handling, and scalability. This makes it suitable for tasks that require fine-grained control over concurrency and error management.

To execute an Effect value, you need a Runtime, which provides the environment necessary to run and manage the computation.

@since2.0.0

@since2.0.0

Effect
<{ readonly
_tag: "user"
_tag
: "user" },
interface Error
Error
>;
const
const exampleEffectHandler: Effect.Effect<HttpServerResponse.HttpServerResponse, never, HttpServerRequest.HttpServerRequest>
exampleEffectHandler
=
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const gen: <YieldWrap<Tag<HttpServerRequest.HttpServerRequest, HttpServerRequest.HttpServerRequest>> | YieldWrap<Effect.Effect<UrlParams.UrlParams, RequestError, never>> | YieldWrap<...> | YieldWrap<...> | YieldWrap<...>, HttpServerResponse.HttpServerResponse>(f: (resume: Effect.Adapter) => Generator<...>) => Effect.Effect<...> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

Example

import { Effect } from "effect"
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, Error> =>
discountRate === 0
? Effect.fail(new Error("Discount rate cannot be zero"))
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function* () {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@since2.0.0

gen
(function* () {
const
const request: HttpServerRequest.HttpServerRequest
request
= yield*
import HttpServerRequest
HttpServerRequest
.
const HttpServerRequest: Tag<HttpServerRequest.HttpServerRequest, HttpServerRequest.HttpServerRequest>

@since1.0.0

@since1.0.0

HttpServerRequest
;
const
const params: UrlParams.UrlParams
params
= yield*
const request: HttpServerRequest.HttpServerRequest
request
.
HttpIncomingMessage<RequestError>.urlParamsBody: Effect.Effect<UrlParams.UrlParams, RequestError, never>
urlParamsBody
;
const
const id: string
id
= yield*
import UrlParams
UrlParams
.
const getFirst: (self: UrlParams.UrlParams, key: string) => Option<string> (+1 overload)

@since1.0.0

getFirst
(
const params: UrlParams.UrlParams
params
, "id").
Pipeable.pipe<Option<string>, Effect.Effect<string, Error, never>>(this: Option<string>, ab: (_: Option<string>) => Effect.Effect<string, Error, never>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const mapError: <NoSuchElementException, Error>(f: (e: NoSuchElementException) => Error) => <A, R>(self: Effect.Effect<A, NoSuchElementException, R>) => Effect.Effect<...> (+1 overload)

Transforms or modifies the error produced by an effect without affecting its success value.

When to Use

This function is helpful when you want to enhance the error with additional information, change the error type, or apply custom error handling while keeping the original behavior of the effect's success values intact. It only operates on the error channel and leaves the success channel unchanged.

Example

import { Effect } from "effect"
// ┌─── Effect<number, string, never>
// ▼
const simulatedTask = Effect.fail("Oh no!").pipe(Effect.as(1))
// ┌─── Effect<number, Error, never>
// ▼
const mapped = Effect.mapError(
simulatedTask,
(message) => new Error(message)
)

@seemap for a version that operates on the success channel.

@seemapBoth for a version that operates on both channels.

@seeorElseFail if you want to replace the error with a new one.

@since2.0.0

mapError
(() => new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
("no id param found")),
);
const
const data: {
readonly _tag: "user";
}
data
= yield*
const doThing: (id: string) => Effect.Effect<{
readonly _tag: "user";
}, Error>
doThing
(
const id: string
id
);
return yield*
import HttpServerResponse
HttpServerResponse
.
const json: (body: unknown, options?: HttpServerResponse.Options.WithContentType | undefined) => Effect.Effect<HttpServerResponse.HttpServerResponse, HttpBodyError>

@since1.0.0

json
(
const data: {
readonly _tag: "user";
}
data
);
}).
Pipeable.pipe<Effect.Effect<HttpServerResponse.HttpServerResponse, Error | RequestError | HttpBodyError, HttpServerRequest.HttpServerRequest>, Effect.Effect<...>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
// probably want some kind of catch all (defects too)
// although the `toWebHandlerRuntime` already does quite a bit for you: https://github.com/Effect-TS/effect/blob/main/packages/platform/src/Http/App.ts#L134
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const catchAllCause: <Error | RequestError | HttpBodyError, HttpServerResponse.HttpServerResponse, never, never>(f: (cause: Cause<Error | RequestError | HttpBodyError>) => Effect.Effect<...>) => <A, R>(self: Effect.Effect<...>) => Effect.Effect<...> (+1 overload)

Handles both recoverable and unrecoverable errors by providing a recovery effect.

When to Use

The catchAllCause function allows you to handle all errors, including unrecoverable defects, by providing a recovery effect. The recovery logic is based on the Cause of the error, which provides detailed information about the failure.

When to Recover from Defects

Defects are unexpected errors that typically shouldn't be recovered from, as they often indicate serious issues. However, in some cases, such as dynamically loaded plugins, controlled recovery might be needed.

Example (Recovering from All Errors)

import { Cause, Effect } from "effect"
// Define an effect that may fail with a recoverable or unrecoverable error
const program = Effect.fail("Something went wrong!")
// Recover from all errors by examining the cause
const recovered = program.pipe(
Effect.catchAllCause((cause) =>
Cause.isFailType(cause)
? Effect.succeed("Recovered from a regular error")
: Effect.succeed("Recovered from a defect")
)
)
Effect.runPromise(recovered).then(console.log)
// Output: "Recovered from a regular error"

@since2.0.0

catchAllCause
((
e: Cause<Error | RequestError | HttpBodyError>
e
) =>
import HttpServerResponse
HttpServerResponse
.
const empty: (options?: HttpServerResponse.Options.WithContent | undefined) => HttpServerResponse.HttpServerResponse

@since1.0.0

empty
().
Pipeable.pipe<HttpServerResponse.HttpServerResponse, HttpServerResponse.HttpServerResponse>(this: HttpServerResponse.HttpServerResponse, ab: (_: HttpServerResponse.HttpServerResponse) => HttpServerResponse.HttpServerResponse): HttpServerResponse.HttpServerResponse (+21 overloads)
pipe
(
import HttpServerResponse
HttpServerResponse
.
const setStatus: (status: number, statusText?: string | undefined) => (self: HttpServerResponse.HttpServerResponse) => HttpServerResponse.HttpServerResponse (+1 overload)

@since1.0.0

setStatus
(500)),
),
);
const
const webHandler: (request: Request, context?: Context<never> | undefined) => Promise<Response>
webHandler
=
import HttpApp
HttpApp
.
const toWebHandlerRuntime: <void>(runtime: Runtime<void>) => <E>(self: HttpApp.Default<E, void | Scope>, middleware?: HttpMiddleware | undefined) => (request: Request, context?: Context<never> | undefined) => Promise<Response>

@since1.0.0

toWebHandlerRuntime
(
const runtime: Runtime<void>
runtime
)(
const effectHandler: Effect.Effect<HttpServerResponse.HttpServerResponse, never, HttpServerRequest.HttpServerRequest>
effectHandler
);
export const
const GET: (req: Request) => Promise<Response>
GET
: (
req: Request
req
:
interface Request
Request
) =>
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
interface Response
Response
> =
const webHandler: (request: Request, context?: Context<never> | undefined) => Promise<Response>
webHandler
;