Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export * from './emails/attachments/interfaces';
export * from './emails/interfaces';
export * from './emails/receiving/interfaces';
export type { ErrorResponse, Response } from './interfaces';
export { Resend } from './resend';
export { Resend, type ResendOptions } from './resend';
export * from './segments/interfaces';
export * from './templates/interfaces';
export * from './topics/interfaces';
Expand Down
135 changes: 135 additions & 0 deletions src/resend.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import createFetchMock from 'vitest-fetch-mock';
import { Resend } from './resend';
import { mockSuccessResponse } from './test-utils/mock-fetch';

const fetchMocker = createFetchMock(vi);
fetchMocker.enableMocks();

describe('Resend', () => {
afterEach(() => fetchMock.resetMocks());
afterAll(() => fetchMocker.disableMocks());

describe('constructor options', () => {
it('uses default baseUrl and userAgent when no options provided', () => {
const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop');

expect(resend.baseUrl).toBe('https://api.resend.com');
expect(resend.userAgent).toMatch(/^resend-node:/);
});

it('uses custom baseUrl when options.baseUrl is provided', () => {
const customBaseUrl = 'https://eu.api.resend.com';
const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', {
baseUrl: customBaseUrl,
});

expect(resend.baseUrl).toBe(customBaseUrl);
});

it('uses custom userAgent when options.userAgent is provided', () => {
const customUserAgent = 'my-app/1.0';
const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', {
userAgent: customUserAgent,
});

expect(resend.userAgent).toBe(customUserAgent);
});

it('uses both custom baseUrl and userAgent when both provided', () => {
const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', {
baseUrl: 'https://custom.api.com',
userAgent: 'custom-agent/2.0',
});

expect(resend.baseUrl).toBe('https://custom.api.com');
expect(resend.userAgent).toBe('custom-agent/2.0');
});

it('uses RESEND_BASE_URL from env when no options.baseUrl provided', () => {
const originalEnv = process.env;
process.env = {
...originalEnv,
RESEND_BASE_URL: 'https://env-base-url.example.com',
};

const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop');
expect(resend.baseUrl).toBe('https://env-base-url.example.com');

process.env = originalEnv;
});

it('uses RESEND_USER_AGENT from env when no options.userAgent provided', () => {
const originalEnv = process.env;
process.env = {
...originalEnv,
RESEND_USER_AGENT: 'env-user-agent/1.0',
};

const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop');
expect(resend.userAgent).toBe('env-user-agent/1.0');

process.env = originalEnv;
});

it('options.baseUrl overrides RESEND_BASE_URL env', () => {
const originalEnv = process.env;
process.env = {
...originalEnv,
RESEND_BASE_URL: 'https://env-base-url.example.com',
};

const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', {
baseUrl: 'https://options-base-url.example.com',
});
expect(resend.baseUrl).toBe('https://options-base-url.example.com');

process.env = originalEnv;
});

it('options.userAgent overrides RESEND_USER_AGENT env', () => {
const originalEnv = process.env;
process.env = {
...originalEnv,
RESEND_USER_AGENT: 'env-user-agent/1.0',
};

const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', {
userAgent: 'options-user-agent/2.0',
});
expect(resend.userAgent).toBe('options-user-agent/2.0');

process.env = originalEnv;
});
});

describe('fetchRequest with custom options', () => {
it('sends request to custom baseUrl', async () => {
const customBaseUrl = 'https://custom.api.resend.com';
const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', {
baseUrl: customBaseUrl,
});

mockSuccessResponse({ id: 'key-123' }, { headers: {} });

await resend.apiKeys.list();

const [url] = fetchMock.mock.calls[0];
expect(url).toBe(`${customBaseUrl}/api-keys`);
});

it('sends custom User-Agent in request headers', async () => {
const customUserAgent = 'my-integration/3.0';
const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', {
userAgent: customUserAgent,
});

mockSuccessResponse({ id: 'key-123' }, { headers: {} });

await resend.apiKeys.list();

const requestOptions = fetchMock.mock.calls[0][1];
const headers = requestOptions?.headers as Headers;
expect(headers.get('User-Agent')).toBe(customUserAgent);
});
});
});
31 changes: 24 additions & 7 deletions src/resend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,27 @@ import { Webhooks } from './webhooks/webhooks';

const defaultBaseUrl = 'https://api.resend.com';
const defaultUserAgent = `resend-node:${version}`;
const baseUrl =
typeof process !== 'undefined' && process.env

function getDefaultBaseUrl(): string {
return typeof process !== 'undefined' && process.env
? process.env.RESEND_BASE_URL || defaultBaseUrl
: defaultBaseUrl;
const userAgent =
typeof process !== 'undefined' && process.env
}

function getDefaultUserAgent(): string {
return typeof process !== 'undefined' && process.env
? process.env.RESEND_USER_AGENT || defaultUserAgent
: defaultUserAgent;
}

export interface ResendOptions {
baseUrl?: string;
userAgent?: string;
}

export class Resend {
readonly baseUrl: string;
readonly userAgent: string;
private readonly headers: Headers;

readonly apiKeys = new ApiKeys(this);
Expand All @@ -45,7 +56,10 @@ export class Resend {
readonly templates = new Templates(this);
readonly topics = new Topics(this);

constructor(readonly key?: string) {
constructor(
readonly key?: string,
options?: ResendOptions,
) {
if (!key) {
if (typeof process !== 'undefined' && process.env) {
this.key = process.env.RESEND_API_KEY;
Expand All @@ -58,16 +72,19 @@ export class Resend {
}
}

this.baseUrl = options?.baseUrl ?? getDefaultBaseUrl();
this.userAgent = options?.userAgent ?? getDefaultUserAgent();

this.headers = new Headers({
Authorization: `Bearer ${this.key}`,
'User-Agent': userAgent,
'User-Agent': this.userAgent,
'Content-Type': 'application/json',
});
}

async fetchRequest<T>(path: string, options = {}): Promise<Response<T>> {
try {
const response = await fetch(`${baseUrl}${path}`, options);
const response = await fetch(`${this.baseUrl}${path}`, options);

if (!response.ok) {
try {
Expand Down
Loading