Browse the docs

Frameworks and agents · Updated 2026-09-28

Send email from NestJS

A NestJS controller and service sending one order confirmation per order, refusing a malformed address first and answering a refusal with its code and fix.

To send email from a NestJS app, put the AgentiSend client in an injectable service and call it from a controller. The files below answer POST /orders/confirmation: the controller refuses a malformed address before the API is called, the service sends once per order however many times the request is retried, and a refusal comes back with the same status and its code and fix. They are executed against a real API on every build of this site's repository, with the Nest app booted and called over HTTP: one send, the same request again replaying it, and one malformed address refused with nothing sent.

Install

pnpm add @nestjs/common @nestjs/core @nestjs/platform-express reflect-metadata rxjs agentisend

Nest's decorators need experimentalDecorators in tsconfig.json, which every Nest project already sets.

Environment

VariableRequiredWhat it is
AGENTISEND_API_KEYyesA key with sending_access.
MAIL_FROMyesThe From address, on a domain you have verified.
AGENTISEND_BASE_URLnoDefaults to https://api.agentisend.com.

The service

/**
 * NestJS — the provider that holds the AgentiSend client and sends the
 * order confirmation.
 */
import { Injectable } from '@nestjs/common';
import { AgentiSend } from 'agentisend';

@Injectable()
export class EmailService {
  /** One client for the process; it reads AGENTISEND_API_KEY and AGENTISEND_BASE_URL. */
  private readonly agentisend = new AgentiSend();

  private mailFrom(): string {
    const from = process.env.MAIL_FROM;
    if (!from) throw new Error('Set MAIL_FROM to an address on a domain you have verified.');
    return from;
  }

  async sendOrderConfirmation(email: string, orderId: string): Promise<string> {
    const { id } = await this.agentisend.emails.send(
      {
        from: this.mailFrom(),
        to: email,
        subject: `Order ${orderId} confirmed`,
        text: `Thanks for your order. Order ${orderId} is confirmed and the receipt is in your account.`,
      },
      // One order, one confirmation: a retried request replays the first send.
      { idempotencyKey: `order-confirmed/${orderId}` },
    );
    return id;
  }
}
examples/nestjs/email.service.ts, executed against the live API on every build

The controller

/**
 * NestJS — `POST /orders/confirmation`, which validates the body, calls the
 * service, and turns a refusal into the same status with `code` and `fix`.
 */
import {
  BadRequestException,
  Body,
  Controller,
  HttpCode,
  HttpException,
  Inject,
  Post,
} from '@nestjs/common';
import { AgentiSendError } from 'agentisend';
import { EmailService } from './email.service.js';

/** Enough to refuse an obvious typo here; the API checks the address properly. */
const ADDRESS = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

@Controller('orders')
export class EmailController {
  // `@Inject` names the provider explicitly, so injection also works under
  // compilers that do not emit decorator metadata (esbuild, Vite, Bun).
  constructor(@Inject(EmailService) private readonly email: EmailService) {}

  @Post('confirmation')
  @HttpCode(200)
  async confirm(@Body() body: { email?: string; orderId?: string }): Promise<{ id: string }> {
    const { email, orderId } = body ?? {};
    if (!email || !ADDRESS.test(email) || !orderId) {
      throw new BadRequestException('email and orderId are required');
    }

    try {
      return { id: await this.email.sendOrderConfirmation(email, orderId) };
    } catch (err) {
      if (err instanceof AgentiSendError) {
        throw new HttpException({ code: err.code, fix: err.fix }, err.status);
      }
      throw err;
    }
  }
}
examples/nestjs/email.controller.ts, executed against the live API on every build

The module

/**
 * NestJS — the module that wires the controller to the service. Nest's
 * decorators need `reflect-metadata` loaded first, so it is the first import.
 */
import 'reflect-metadata';
import { Module } from '@nestjs/common';
import { EmailController } from './email.controller.js';
import { EmailService } from './email.service.js';

@Module({
  controllers: [EmailController],
  providers: [EmailService],
})
export class AppModule {}
examples/nestjs/app.module.ts, executed against the live API on every build

Start it

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

const app = await NestFactory.create(AppModule);
await app.listen(3000);
main.ts
curl -X POST localhost:3000/orders/confirmation \
  -H 'content-type: application/json' \
  -d '{"email":"you@example.com","orderId":"A-1042"}'

The key and its budget

Give the app a key of its own rather than the one you signed in with. POST /api-keys with sending_access mints a key that can send and read what it sent, and nothing else. PATCH /limits/keys/{id} puts a ceiling on it with budget_per_period and period; past the ceiling a send is refused with agent_budget_exceeded, and the controller answers with that code and its fix instead of retrying.

The idempotency key

Every send carries Idempotency-Key: order-confirmed/<orderId>, derived from the order and never from the moment. A retry after a timeout gets the first send's id back instead of confirming the same order twice.

Next