v9.1.0

Next.js

Read typed config in a Next.js app from server code, client components, and edge routes, and catch a missing variable at build time or at server start.

Next.js loads your .env files into process.env, and with plain process.env every value is a string. process.env.MAX_CONNECTIONS held '10'. A typo in DATABASE_URL appeared on the first query that used it. Each file that read a variable set its own default.

Envapter reads that same process.env and converts each value when you read it. Code on the Node.js runtime loads envapt's node build, which reads process.env and your .env files with no setup. Client components and edge routes load the portable build, which reads nothing until you pass it a source with Envapter.useSource.

Server code

lib/config.ts
import { ,  } from 'envapt';

export const  = .(
    {
        : .,
        : .,
        : .
    },
    'camelCase'
);

getRequiredAll converts each key with its converter, and 'camelCase' turns MAX_CONNECTIONS into config.maxConnections, a number. config.sessionTtl is a number of milliseconds and config.databaseUrl is a URL. If keys are missing, it throws one MissingEnvValue error that lists them all. Structuring your config explains why every read goes through one module.

Failed to collect page data

next build imports every route module while it collects page data, including dynamic routes, so config runs during the build too. If a required variable is missing on the build machine, the build stops:

Error: Failed to collect page data for /api/orders
  [cause]: EnvaptError [305]: Missing required environment variables: DATABASE_URL.

305 is MissingEnvValue. If the build machine has the same variables as the server, that build failure catches a missing variable before you deploy. If you build somewhere without your secrets, such as a Docker image that gets them when it starts, export a function:

lib/config.ts
export function () {
    return .(
        {
            : .,
            : .,
            : .
        },
        'camelCase'
    );
}

serverConfig() reads the variables each time you call it. When Next.js collects page data, it imports the module without calling the function, so the build no longer checks your config. Call serverConfig() in register() to check it at server start, as the next section shows.

Static pages keep the values from the build

If a page reads nothing from the request, Next.js renders it once during next build and serves that HTML. A value it reads through config or Envapter.get stays at whatever the build machine had until you build again. To read the value on each request, call connection() first:

app/status/page.tsx
export default async function () {
    await ();
    return <>Serving from {.('DEPLOY_REGION', 'local')}</>;
}

DEPLOY_REGION then comes from the environment the server is running with, and 'local' is the fallback when it's unset.

Checking config when the server starts

Without a startup check, a missing variable fails only the first request that imports config. That route returns 500 while the others keep working. Next.js calls register() in instrumentation.ts when the server starts, before it serves a request, so importing config there checks the required keys at startup:

instrumentation.ts
export async function register() {
    if (process.env.NEXT_RUNTIME === 'nodejs') {
        await import('./lib/config');
    }
}

If your config module exports serverConfig(), call it inside the if in place of the import. Next.js also runs register() on the edge runtime, where envapt resolves to the portable build. A read there before useSource throws NoSourceBound, and the NEXT_RUNTIME check keeps the import on the Node.js server.

If register() throws, Next.js logs "Failed to prepare server", but the process keeps listening. Every request returns 500 with the same error. A health check that only tests whether the port is open still passes, so point it at a route that imports config.

Which .env file wins

Next.js picks its .env files by NODE_ENV and copies them into process.env before any of your code runs. By default, envapt ranks a value already in process.env above every file, so for each key that Next.js loads, this order sets the value:

next start (NODE_ENV=production)
├─ the shell               highest priority
├─ .env.production.local
├─ .env.local
├─ .env.production
└─ .env

.env.local wins over .env.production here, the reverse of envapt's own order. next dev reads the .env.development files in place of the .env.production ones.

NODE_ENV also sets Envapter.environment when ENVIRONMENT and ENV are unset. next build and next start set it to production, and next dev sets development.

With ENVIRONMENT=staging, Envapter.environment is 'staging', so envapt reads .env.staging, a file Next.js never loads. Next.js still loads .env.production, because it picks files only by NODE_ENV. If a key is set in both files, envapt reads the .env.production value. Keep staging values out of .env.production, or set them in your host's environment.

Client components

A client component runs in the browser. When Next.js builds the bundle, it replaces each process.env.NEXT_PUBLIC_* reference that spells out the variable's name with its value, and process.env.NODE_ENV too. The client bundle loads the portable build, which reads nothing until you bind a source. Bind an object that spells out each name:

lib/public-config.ts
.(
    new ({
        : ..,
        : ..,
        : ..
    })
);

export const  = .('NEXT_PUBLIC_API_URL', .);
export const  = .('NEXT_PUBLIC_BETA', ., false);

PortableSource holds only the keys in that object. A variable you leave out reads as unset, so getRequired throws for it and getAs returns its fallback. When you add a NEXT_PUBLIC_ variable, add it to the object too.

betaEnabled is a boolean, where process.env.NEXT_PUBLIC_BETA holds the string 'false' when the flag is off. NODE_ENV is in the object because Envapter.environment reads it from the source. Without it, Envapter.environment is 'development' in the browser.

Import public-config.ts only from files that start with 'use client', for the reason in the next section. If one component needs one public value, read it in a server component and pass it down as a prop, which keeps envapt out of the client bundle.

Keep envapt out of serverExternalPackages

Next.js also renders client components on the server, and it bundles a separate copy of envapt for that render. The useSource call in public-config.ts replaces the source only in that copy, so your server code keeps reading process.env and your .env files.

If you list envapt in serverExternalPackages, both renders load one shared copy. The client's useSource call then replaces the source your server code reads. next build stops with a missing-variable error for server keys like DATABASE_URL.

Edge routes

A route that exports runtime = 'edge' loads the portable build. Bind process.env before the first read:

app/api/geo/route.ts
export const  = 'edge';

.(new (.));
const  = .('GEO_API_URL', .);

export function () {
    return ();
}

getRequired comes after useSource. If you swap the two lines, the read throws NoSourceBound (307). The read runs at module scope, so a missing GEO_API_URL stops next build the same way a server route does. Outside Next.js, Vercel's Edge runtime binds the same way.

Decorators

Next.js compiles your TypeScript with SWC. Next.js turns on SWC's decorator support when tsconfig.json sets experimentalDecorators, and it compiles only legacy decorators. Set the flag, then import @Envapt from envapt/legacy and declare static readonly fields:

tsconfig.json
{ "compilerOptions": { "experimentalDecorators": true } }
lib/AppConfig.ts
import {  } from 'envapt';
import {  } from 'envapt/legacy';

export class  {
    @('DATABASE_URL', { : ., : true })
    static readonly : ;

    @('SESSION_TTL', { : ., : '15m' })
    static readonly : number;
}

Converters comes from envapt, and only Envapt comes from envapt/legacy. A decorated field reads its variable the first time you access it, so importing AppConfig in register() checks nothing. Read AppConfig.databaseUrl there to check it at startup. The legacy decorators section has the instance-field form.

not implemented: ClassMember::AutoAccessor

next build stops with this error on a modern static accessor field imported from envapt. In Next.js 16.4, Turbopack, the bundler next build uses by default, can't parse the accessor keyword. Switch the field to the static readonly form above.

On this page