Creating and Providing Layers

Avatar of Hemanta SundarayHemanta Sundaray

A Layer is how you fulfill a requirement. It runs the make effect and registers the result under the service tag.

We create a Layer with Layer.effect, which takes two arguments:

  1. The service tag
  2. The Effect that produces the service implementation
email.ts
import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";
import { Effect, Option, ServiceMap, Schema, Layer } from "effect";
66 collapsed lines
class SendPasswordResetEmailError extends Schema.TaggedErrorClass<SendPasswordResetEmailError>()(
"SendPasswordResetEmailError",
{
message: Schema.String.pipe(
Schema.withConstructorDefault(() =>
Option.some("Failed to send password reset email"),
),
),
cause: Schema.Defect,
},
) {}
export class Email extends ServiceMap.Service<Email>()("app/Email", {
make: Effect.gen(function* () {
const sesClient = new SESClient({
region: "us-east-1",
credentials: {
accessKeyId: "your-access-key-id",
secretAccessKey: "your-secret-access-key",
},
});
// ============ Send Password Reset Email ============
function sendPasswordResetEmail(to: string, resetLink: string) {
return Effect.tryPromise({
try: () =>
sesClient.send(
new SendEmailCommand({
Destination: { ToAddresses: [to] },
Message: {
Body: {
Html: {
Charset: "UTF-8",
Data: `
<h1>Password Reset</h1>
<p>Click the link below to reset your password:</p>
<a href="${resetLink}">Reset Password</a>
`,
},
},
Subject: {
Charset: "UTF-8",
Data: "Reset Your Password",
},
},
Source: "noreply@yourapp.com",
}),
),
catch: (error) => new SendPasswordResetEmailError({ cause: error }),
}).pipe(Effect.asVoid),
}
return { sendPasswordResetEmail };
}),
}) {}
function handleForgotPassword(email: string) {
return Effect.gen(function* () {
const emailService = yield* Email;
yield* emailService.sendPasswordResetEmail(
email,
`https://yourapp.com/reset?token=123abc`,
);
});
}
const EmailLive = Layer.effect(Email, Email.make);
const program = handleForgotPassword("user@example.com");
Effect.runPromise(program);
// ^^^^^^^ Type Error!

This says: run Email.make, take the { sendPasswordResetEmail } object it returns, and register it under the Email tag. From that point on, any code that does yield* Email will receive that { sendPasswordResetEmail } object.

Above, we defined EmailLive outside the Email class. You can also define it as a static property inside the class body. That way the service definition and its layer live together in one place:

email.ts
import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";
import { Effect, Option, ServiceMap, Schema, Layer } from "effect";
class SendPasswordResetEmailError extends Schema.TaggedErrorClass<SendPasswordResetEmailError>()(
"SendPasswordResetEmailError",
{
message: Schema.String.pipe(
Schema.withConstructorDefault(() =>
Option.some("Failed to send password reset email"),
),
),
cause: Schema.Defect,
},
) {}
export class Email extends ServiceMap.Service<Email>()("app/Email", {
make: Effect.gen(function* () {
const sesClient = new SESClient({
region: "us-east-1",
credentials: {
accessKeyId: "your-access-key-id",
secretAccessKey: "your-secret-access-key",
},
});
// ============ Send Password Reset Email ============
28 collapsed lines
function sendPasswordResetEmail(to: string, resetLink: string) {
return Effect.tryPromise({
try: () =>
sesClient.send(
new SendEmailCommand({
Destination: { ToAddresses: [to] },
Message: {
Body: {
Html: {
Charset: "UTF-8",
Data: `
<h1>Password Reset</h1>
<p>Click the link below to reset your password:</p>
<a href="${resetLink}">Reset Password</a>
`,
},
},
Subject: {
Charset: "UTF-8",
Data: "Reset Your Password",
},
},
Source: "noreply@yourapp.com",
}),
),
catch: (error) => new SendPasswordResetEmailError({ cause: error }),
}).pipe(Effect.asVoid),
}
return { sendPasswordResetEmail };
}),
}) {
static Live = Layer.effect(this, this.make);
}
10 collapsed lines
function handleForgotPassword(email: string) {
return Effect.gen(function* () {
const emailService = yield* Email;
yield* emailService.sendPasswordResetEmail(
email,
`https://yourapp.com/reset?token=123abc`,
);
});
}
const program = handleForgotPassword("user@example.com");
Effect.runPromise(program);
// ^^^^^^^ Type Error!

Inside the class body, this refers to the Email class itself. So Layer.effect(this, this.make) is the same as writing Layer.effect(Email, Email.make).

Now we have a service (Email), a function that uses it (handleForgotPassword), and a layer that knows how to provide it (Email.Live). Let’s wire it all together and actually run the program.

We use Effect.provide to give a layer to a program:

email.ts
import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";
import { Effect, Option, ServiceMap, Schema, Layer } from "effect";
class SendPasswordResetEmailError extends Schema.TaggedErrorClass<SendPasswordResetEmailError>()(
"SendPasswordResetEmailError",
{
message: Schema.String.pipe(
Schema.withConstructorDefault(() =>
Option.some("Failed to send password reset email"),
),
),
cause: Schema.Defect,
},
) {}
export class Email extends ServiceMap.Service<Email>()("app/Email", {
make: Effect.gen(function* () {
const sesClient = new SESClient({
region: "us-east-1",
credentials: {
accessKeyId: "your-access-key-id",
secretAccessKey: "your-secret-access-key",
},
});
// ============ Send Password Reset Email ============
function sendPasswordResetEmail(to: string, resetLink: string) {
return Effect.tryPromise({
try: () =>
sesClient.send(
new SendEmailCommand({
Destination: { ToAddresses: [to] },
Message: {
Body: {
Html: {
Charset: "UTF-8",
Data: `
<h1>Password Reset</h1>
<p>Click the link below to reset your password:</p>
<a href="${resetLink}">Reset Password</a>
`,
},
},
Subject: {
Charset: "UTF-8",
Data: "Reset Your Password",
},
},
Source: "noreply@yourapp.com",
}),
),
catch: (error) => new SendPasswordResetEmailError({ cause: error }),
}).pipe(Effect.asVoid),
}
return { sendPasswordResetEmail };
}),
}) {
static Live = Layer.effect(this, this.make);
}
function handleForgotPassword(email: string) {
return Effect.gen(function* () {
const emailService = yield* Email;
yield* emailService.sendPasswordResetEmail(
email,
`https://yourapp.com/reset?token=123abc`,
);
});
}
const program = handleForgotPassword("user@example.com");
Effect.runPromise(program.pipe(Effect.provide(Email.Live)));

Notice that there is no red squiggly error under program this time. That is because we provided the Email dependency to the program using Effect.provide(Email.Live).

Here is the flow when handleForgotPassword runs:

  1. Effect.runPromise starts the runtime.
  2. The runtime sees that the program requires Email.
  3. It runs Email.Live, which executes Email.make. That creates the SES client and returns the { sendPasswordResetEmail } object.
  4. The runtime registers that object under the Email tag.
  5. The program starts running. When it hits yield* Email, the runtime looks up the tag and hands back the { sendPasswordResetEmail } object.
  6. The function calls emailService.sendPasswordResetEmail(...) and the email is sent.
  7. Effect.runPromise returns a Promise that resolves when the effect completes.

Congratulations! You wrote your first Effect service.

Let’s circle back to the point I made at the start of this chapter. I said that Effect Service is a system that automatically makes dependencies available to any function that needs them, no matter how deep in the call chain, and tracks all of this at the type level so the compiler tells us if we forgot to provide something.

You saw this in action. When we forgot to provide the Email dependency to our program, the compiler gave us a TypeScript error. And if you decide to use the Email service anywhere in your application, all you need to do is access it using yield* Email and you get access to all the functions defined in that service. I hope you now understand the benefits Effect Services provide when it comes to dependency management.

Sign in to save progress

Stay in the loop

Get notified when new Effect Atom related content is published.