Skip to content

Quick Start

This guide walks you through adding the auth domain to an existing TypeScript project. By the end you will have a working user registration and login system with typed errors, port interfaces, and a Prisma adapter — all in your repository.

If you have not already initialized Backcap in your project:

Terminal window
npx @backcap/cli init

Accept the detected framework and package manager, or select them manually. A backcap.json file will be created in your project root.

Terminal window
npx @backcap/cli add auth

The CLI will:

  1. Fetch the auth domain bundle from the registry
  2. Check for any file conflicts with your existing codebase
  3. Ask you to confirm the installation
  4. Write the domain source files to src/domains/auth/
  5. Install any required npm dependencies
  6. Record auth in your backcap.json
src/domains/auth/
domain/
entities/
user.entity.ts # User aggregate root
value-objects/
email.vo.ts # Email with RFC-5321 validation
password.vo.ts # Password with strength validation
errors/
invalid-email.error.ts
invalid-credentials.error.ts
user-not-found.error.ts
user-already-exists.error.ts
events/
user-registered.event.ts
__tests__/
user.entity.test.ts
email.vo.test.ts
password.vo.test.ts
errors.test.ts
user-registered.event.test.ts
application/
use-cases/
register-user.use-case.ts
login-user.use-case.ts
ports/
user-repository.port.ts # IUserRepository interface
password-hasher.port.ts # IPasswordHasher interface
token-service.port.ts # ITokenService interface
dto/
register-input.dto.ts
login-input.dto.ts
login-output.dto.ts
__tests__/
register-user.use-case.test.ts
login-user.use-case.test.ts
mocks/
user-repository.mock.ts
password-hasher.mock.ts
token-service.mock.ts
fixtures/
user.fixture.ts
contracts/
auth.contract.ts # IAuthService interface
auth.factory.ts # createAuthService() factory
index.ts
shared/
result.ts # Result<T, E> monad

The auth domain defines three port interfaces that you must implement. These are the seams between the domain logic and your infrastructure:

src/domains/auth/application/ports/user-repository.port.ts
export interface IUserRepository {
findByEmail(email: string): Promise<User | null>;
save(user: User): Promise<void>;
findById(id: string): Promise<User | null>;
}

If you are using Prisma, implement IUserRepository by writing a PrismaUserRepository class. For example:

src/adapters/prisma/auth/user-repository.adapter.ts
import type { IUserRepository } from "../../domains/auth/application/ports/user-repository.port";
// ... implement save, findByEmail, findById using your Prisma client

Implement this interface using bcrypt or argon2:

src/adapters/my-app/password-hasher.adapter.ts
import bcrypt from "bcrypt";
import type { IPasswordHasher } from "../../domains/auth/application/ports/password-hasher.port";
export class BcryptPasswordHasher implements IPasswordHasher {
async hash(plain: string): Promise<string> {
return bcrypt.hash(plain, 10);
}
async compare(plain: string, hash: string): Promise<boolean> {
return bcrypt.compare(plain, hash);
}
}

Implement using jsonwebtoken or any JWT library:

src/adapters/my-app/token-service.adapter.ts
import jwt from "jsonwebtoken";
import type { ITokenService } from "../../domains/auth/application/ports/token-service.port";
export class JwtTokenService implements ITokenService {
constructor(private readonly secret: string) {}
async generate(userId: string, roles: string[]): Promise<string> {
return jwt.sign({ userId, roles }, this.secret, { expiresIn: "7d" });
}
async verify(token: string): Promise<{ userId: string } | null> {
try {
return jwt.verify(token, this.secret) as { userId: string };
} catch {
return null;
}
}
}

Use the createAuthService factory from the contracts layer to assemble the service:

src/container.ts
import { createAuthService } from "./domains/auth/contracts";
import { PrismaUserRepository } from "./adapters/prisma/auth/user-repository.adapter";
import { BcryptPasswordHasher } from "./adapters/my-app/password-hasher.adapter";
import { JwtTokenService } from "./adapters/my-app/token-service.adapter";
import { prisma } from "./lib/prisma"; // your Prisma client
export const authService = createAuthService({
userRepository: new PrismaUserRepository(prisma),
passwordHasher: new BcryptPasswordHasher(),
tokenService: new JwtTokenService(process.env.JWT_SECRET!),
});

Call the service from your route handlers or controllers:

// Registration
const result = await authService.register({
email: "user@example.com",
password: "securepassword1",
});
if (result.isFail()) {
const error = result.unwrapError();
// error is typed: UserAlreadyExists | InvalidEmail | Error
console.error(error.message);
return;
}
const { userId } = result.unwrap();
// Login
const loginResult = await authService.login({
email: "user@example.com",
password: "securepassword1",
});
if (loginResult.isOk()) {
const { token, userId } = loginResult.unwrap();
}

If you are using Express (or any other framework), implement an HTTP adapter that calls your authService. For example:

import express from "express";
import { authService } from "./container";
const app = express();
app.use(express.json());
app.post("/auth/register", async (req, res) => {
const result = await authService.register(req.body);
if (result.isFail()) {
return res.status(400).json({ error: result.unwrapError().message });
}
return res.status(201).json(result.unwrap());
});

You own this wiring code — implement it in whatever way fits your framework.