I built my Auth API with Effect - Part 1
Discover how to build an authentication API using Effect in TypeScript, focusing on declarative composition, robust error handling, and modular architecture.
3 juillet 2025
Published
Hugo Mufraggi
Author

I Built My Auth API With Effect part 1 page
🚀 TL;DR
Building an authentication API with Effect might seem daunting, but it’s simpler than you think.
This tutorial demystifies @effect/platform by building a complete auth API step-by-step. You'll discover how Effect transforms async effect management through:
- Declarative composition: Separate contracts from implementations for testable code
- Robust error handling: Strong typing that eliminates production surprises
- Modular architecture: Reusable and swappable layers
- Natural TDD: Test your effects without spinning up HTTP servers
Result: A type-safe, maintainable, production-ready TypeScript API.
Introduction
In this article, I’ll take a deep dive into the @effect/platform type system by building an authentication API. My goal is to make endpoint declaration with Effect easier to understand and more approachable.
When you first start using Effect, it can be challenging to grasp how effects work and how to build real-world applications with them. This article walks you through that process step by step.
Why Effect?
Effect is a powerful TypeScript library designed to tackle the complexity of managing asynchronous effects safely and predictably. It relies on advanced functional programming concepts and strong static typing to offer:
- Simplified composition of asynchronous programs
- Strong error handling
- A declarative, pure approach to managing side effects
It promotes writing robust, composable, and testable code in a modern TypeScript environment.
Why @effect/platform?
The @effect/platform package provides abstractions for building platform-independent applications that run across Node.js, Deno, Bun, and even the browser.
With @effect/platform, you can define abstract services like FileSystem or Terminal and later provide concrete implementations (called layers) for your target environment.
In this guide, I’ll mainly use the HttpServer module from @effect/platform to build and expose my HTTP API.

Composition: Declarative by Design
In Effect, everything revolves around declarative composition — from dependencies to API routes.
For each group of endpoints, we typically write two separate parts:
- The contract — what we want to expose (like an interface)
- The implementation — the logic that fulfills the contract
This separation offers several advantages:
- Testability — Contracts and handlers can be tested in isolation, without spinning up an HTTP server.
- Decoupling — Each endpoint is independent, enabling multiple implementations and promoting IoC (Inversion of Control).
- Recomposition — Layers let you reuse or swap out API components for modular, flexible architectures.
- Strong Typing — Contracts are fully typed, reducing errors and serving as live documentation (great for API-first design).
- Interoperability — Makes it easy to auto-generate documentation or clients for full-stack TypeScript development.
- Maintainability — A clear structure improves readability and understanding of where functionality lives.
Practice Time: Building an Auth API
In this article, we’ll build three key components:
- An Auth Repository to interact with the database
- An Encrypt Service to hash passwords
- The Auth Service, which wires the two together and exposes endpoints
Step 0: Project Initialization
Start by checking the official Effect documentation. Personally, I use Bun, but feel free to use any runtime or package manager you’re comfortable with.
bash
CopierModifier
bunx create-effect-app@latest
Here’s how I organize my codebase (inspired by vertical slicing):
bash
CopierModifier
src
├── Program.ts
└── pokemon
├── domain
├── get-by-id
└── list
💡 TODO: Update paths if needed for your project structure.
Step 0.5: SQL Migrations
Our Auth Repository will implement just two endpoints for now:
createUsergetUserByEmail
We’ll run PostgreSQL using Docker and manage migrations via my personal seed repository (which wraps dbmate).
Migration SQL:
sql
CopierModifier
CREATE TABLE users (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
email varchar(255) NOT NULL UNIQUE,
password varchar(255) NOT NULL,
created_at timestamptz DEFAULT NOW(),
updated_at timestamptz DEFAULT NOW()
);
Let’s create a Sql.ts file to expose a reusable PostgreSQL client:
import { PlatformConfigProvider } from "@effect/platform"
import { NodeContext } from "@effect/platform-node"
import { PgClient } from "@effect/sql-pg"
import { Config, Effect, identity, Layer, Redacted, String } from "effect"
import * as path from "node:path"
export const PgLive = Layer.unwrapEffect(
Effect.gen(function* () {
const database = yield* Config.string("DB_HOST")
const username = yield* Config.string("DB_USER")
const port = yield* Config.string("DB_PORT")
const password = yield* Config.string("DB_PWD")
const dbName = yield* Config.string("DB_NAME")
const env = yield* Config.string("ENV")
let url = `postgres://${username}:${password}@${database}:${port}/${dbName}`
if (env === "production") {
url += "?sslmode=require"
}
return PgClient.layer({
url: Redacted.make(url),
transformQueryNames: String.camelToSnake,
transformResultNames: String.snakeToCamel,
types: {
114: { to: 25, from: [114], parse: identity, serialize: identity },
1082: { to: 25, from: [1082], parse: identity, serialize: identity },
1114: { to: 25, from: [1114], parse: identity, serialize: identity },
1184: { to: 25, from: [1184], parse: identity, serialize: identity },
3802: { to: 25, from: [3802], parse: identity, serialize: identity }
}
})
})
).pipe(
Layer.provide(PlatformConfigProvider.layerDotEnv(path.join(process.cwd(), ".env"))),
Layer.provide(NodeContext.layer)
)
You can now inject PgLive anywhere in your project to access the database.
Step 1: SQL Repository
The more I use Effect, the more I realize how naturally it supports test-driven development (TDD).
Why? Because you can start by defining your return type using Effect.Effect<R, E, A>:
A– the success valueE– the error type(s)R– the required environment (dependencies)
This makes it super easy to write tests before the actual implementation. Here’s an example:
describe("User Repository", () => {
it("should create a user", async () => {
const program = Effect.gen(function* () {
return yield* repository.insert({
password: passwordFromString("aaaaaa"),
email: Email.make("email@gmail.com"),
createdAt: undefined,
updatedAt: undefined
})
})
const result = await Effect.runPromise(
program.pipe(
Effect.provide(UserRepository.Default)
)
)
expect(result.email).toBe("email@gmail.com")
})
})
This test is clean, typed, and isolated. We’re not even running an HTTP server — just calling the effect.
Let’s define the UserModel:
import { HttpApiSchema } from "@effect/platform"
import { Model } from "@effect/sql"
import { Context, Schema } from "effect"
import { Email } from "./email.js"
import { Password } from "./Password.js"
export const UserId = Schema.UUID.pipe(Schema.brand("UserId"))
export type UserId = typeof UserId.Type
export class User extends Model.Class<User>("User")({
id: Model.Generated(UserId),
email: Email,
password: Model.Sensitive(Password),
createdAt: Model.DateTimeInsert,
updatedAt: Model.DateTimeUpdate
}) {}
export class UserWithSensitive extends Model.Class<UserWithSensitive>("UserWithSensitive")({
...Model.fields(User),
password: Password
}) {}
export class CurrentUser extends Context.Tag("Domain/User/CurrentUser")<CurrentUser, User>() {}
export class UserNotFound extends Schema.TaggedError<UserNotFound>()(
"UserNotFound",
{ id: UserId },
HttpApiSchema.annotations({ status: 404 })
) {}
Now you’re ready to implement the actual UserRepository. It’s fairly simple—let me explain.
The UserRepository is an Effect.Service. An Effect.Service is a type-safe way to define and provide layers of dependencies.
Inside it, I define two keys:
dependencies– an array of required layers (here,PgLive)effect– a generator function that returns the repository
Thanks to the User model, I can define a repository. It’s really useful—it provides built-in methods like insert, update, getById, delete, etc.
import { Model, SqlClient, SqlSchema } from "@effect/sql"
import { Effect, pipe } from "effect"
import { makeTestLayer } from "../lib/Layer.js"
import { PgLive } from "../Sql.js"
import { Email } from "./domain/email.js"
import { User } from "./domain/User.js"
export class UserRepository extends Effect.Service<UserRepository>()("UserRepository", {
effect: Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient
const repo = yield* Model.makeRepository(User, {
tableName: "users",
spanPrefix: "UsersRepo",
idColumn: "id"
})
const findByEmailSchema = SqlSchema.findOne({
Request: Email,
Result: User,
execute: (key) => sql`select * from users where email = ${key}`
})
const findByEmail = (email: Email) =>
pipe(
findByEmailSchema(email),
Effect.orDie,
Effect.withSpan("UsersRepository.findByEmail")
)
return { ...repo, findByEmail }
}),
dependencies: [PgLive]
}) {
static Test = makeTestLayer(UserRepository)({})
}
In this example, I needed to define a custom query getByEmail.
In Effect’s declarative approach, I first define my query schema:
Request– inputResult– expected outputexecute– the actual SQL query
Then I define my function findByEmail that uses this query. Finally, I return an object spreading the default repo methods and adding my custom one.
Conclusion
Congratulations! You’ve built the foundation of a robust authentication API with Effect. But this is just the beginning.
Effect revolutionizes how we write TypeScript by bringing the power of functional programming without the usual complexity. With today’s patterns, you can now build type-safe, testable APIs with predictable error handling.
Ready for more? Upcoming articles will cover JWT authentication, advanced error handling, and production deployment with Effect.