Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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