In the previous chapter, I said that Effect Service is a system that automatically makes dependencies available to any part of your program 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.
Let’s see it in action.
Let’s create a service named Email:
import { Effect, ServiceMap } from "effect";
class Email extends ServiceMap.Service<Email>()("app/Email", { make: Effect.gen(function* () { // we'll fill this in shortly }),}) {}A service is defined using a class that extends ServiceMap.Service. ServiceMap.Service is a utility provided by Effect that lets you create a service tag. A service tag is a unique identifier that represents a dependency your program needs.
In the code above, "app/Email" is the unique identifier. The class name Email is what you use in your code to refer to the service, but "app/Email" is the string that Effect uses internally to identify it.
Let’s look at the syntax more closely.
ServiceMap.Service<Email>()("app/Email", { make: ... })You see two sets of parentheses after ServiceMap.Service<Email> because it is a curried function. A curried function is a function that returns another function. The first call returns a function, which is then immediately called with two arguments.
The first argument is the unique string identifier. It can be any string you want. The only rule is that it must be unique across your entire application. Here we chose "app/Email". A common convention is to prefix with your app or module name to avoid collisions.
The second argument is an options object. It has a property called make. The name must be make exactly, and the value of make must be an Effect.
Now let’s fill in make with a real implementation. Our Email service will expose one operation, sendPasswordResetEmail, which takes a recipient and a reset link and sends the email:
import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";import { Effect, Schema, ServiceMap } from "effect";
class SendPasswordResetEmailError extends Schema.TaggedErrorClass<SendPasswordResetEmailError>()( "SendPasswordResetEmailError", { message: Schema.String.pipe( Schema.withConstructorDefault( () => "Failed to send the password reset email", ), ), cause: Schema.Defect, },) {}
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", }, });
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 }; }),}) {}We define an error named SendPasswordResetEmailError using Schema.TaggedErrorClass. It has two fields: message, which defaults to “Failed to send the password reset email” if not provided, and cause, which is typed as Schema.Defect to capture the underlying error that caused the failure.
Then inside make, we create the SES client with its region and credentials. We define a sendPasswordResetEmail function that uses the client to send the email. Finally, we return { sendPasswordResetEmail } so that anyone who uses this service gets access to that function.