Next.js Build Breaks on a Module-Level Resend Client
This article may contain affiliate links. Its content is not affected by advertising.
In short
Calling new Resend(...) at module scope means the key is validated on import, so next build's page-data collection fails the whole build without RESEND_API_KEY.
The short version
Instantiating the Resend client at module scope means merely import-ing that file runs the API key check. On an environment without RESEND_API_KEY, the exception fires at import time, before any email is actually sent — and because next build imports the module during page-data collection, the build itself fails before a single request comes in. The fix is to move new Resend(...) out of module scope and into the function that calls it, so it becomes lazy initialisation that only runs when actually invoked.
What it happened
We hit this right after the initial implementation. sendResultEmail, which sends the certification-exam score email, is called from the API route src/app/api/admin/score/route.ts. That route itself is dynamic and isn’t statically generated at build time. Even so, next build loads each route’s module once during page-data collection to read its configuration, so the moment route.ts did import { sendResultEmail } from '@/lib/email', the top-level code in email.ts was evaluated.
In a build environment where RESEND_API_KEY isn’t injected (CI, preview environments, and the like), that load throws for the reason below, failing the whole build. It won’t reproduce locally with a key in .env.local, and it won’t reproduce building against an environment where a production key is set — it only shows up in a build environment where the key is deliberately withheld.
Why
Before the fix, src/lib/email.ts looked like this:
import { Resend } from 'resend'
const resend = new Resend(process.env.RESEND_API_KEY)
interface ResultEmailProps {
email: string
level: number
verdict: 'passed' | 'failed'
feedback?: string
}
export async function sendResultEmail({ email, level, verdict, feedback }: ResultEmailProps) {
const isPassed = verdict === 'passed'
// ...
}
new Resend(process.env.RESEND_API_KEY) sits outside sendResultEmail, at the top level of the module. JavaScript evaluates top-level module code the moment it’s imported, so this line runs as soon as email.ts is imported, even if sendResultEmail is never called.
Resend’s constructor is implemented to throw if it isn’t given a valid API key. If RESEND_API_KEY isn’t in the environment, process.env.RESEND_API_KEY is undefined and the constructor throws immediately. That’s not a runtime “failed to send email” error — it’s an exception that fails the module load itself, and it fires the instant next build’s page-data collection tries to load that module, stopping the build process entirely.
Fixing it
We moved new Resend(...) out of module scope and into the sendResultEmail function:
import { Resend } from 'resend'
interface ResultEmailProps {
email: string
level: number
verdict: 'passed' | 'failed'
feedback?: string
}
export async function sendResultEmail({ email, level, verdict, feedback }: ResultEmailProps) {
const resend = new Resend(process.env.RESEND_API_KEY)
const isPassed = verdict === 'passed'
// ...
}
Nothing is evaluated when email.ts is imported anymore. new Resend(...) only runs once sendResultEmail is actually called — lazy initialisation. It doesn’t execute during the build, when the API route is merely imported; it only reads whatever RESEND_API_KEY is injected into that environment at runtime, when a request is actually being handled.
Preventing a repeat
Many SDK clients like this one validate the presence of a key immediately in their constructor. Instantiating one at module scope means that any path that imports that file — including build-time page-data collection — inherits the failure of a missing key. The safer default is to create an external SDK client inside the function or request handler that actually uses it, keeping a bare import from executing anything.
Frequently asked questions
Q1Why does instantiating Resend at module scope cause a problem?
JavaScript evaluates top-level module code as soon as it is imported. Resend's constructor throws if no API key is supplied, so calling new Resend(...) at module scope means merely importing that file triggers the exception, whether or not the caller ever actually sends an email.
Q2Why does it fail at build time instead of runtime?
The function lives in a Next.js API route (route.ts). next build imports every route's module once to collect page data, so the module-level exception fires during that collection step and fails the build itself, before any request has arrived.
Q3What kind of environment lacks RESEND_API_KEY?
CI and preview environments that don't have production secrets injected. Local development with the key in .env.local won't reproduce it, and a build against an environment with the key set won't either — it only surfaces where the key is deliberately withheld.
Q4What changed in the fix?
Moving new Resend(...) out of module scope and into the function that actually sends the email (sendResultEmail). Nothing runs on import; the API key is only read once the function is called, making it lazy initialisation.
Environment verified
- resend ^6.17.2 / Next.js 16.2.10 / React 19.2.4
- Found and fixed same-day on 2026-07-13, right after initial implementation
What this article is based on
- TypeScript file lines 1-4commit 1b56023
- TypeScript file lines 1-11commit a949c8d
- TypeScript file lines 1-3commit 1b56023
Every claim in this article comes from the records above. The repositories we operate are private so we cannot link to them, but which file, which lines, and at which commit we read them is recorded for every article. Nothing here is written from guesswork.