skip to Main Content

After a bunch of false starts I’ve followed the T3 stack approach to runtime environment variables for my next.js app, particularly to protect my MONGODB_URI secret by providing it only at runtime on the server.

env/server.js:

import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({
  server: {
    NODE_ENV: z.enum(["development", "test", "production"]),
    MONGODB_URI: z.string().url(),
  },
  runtimeEnv: process.env,
});

lib/mongodb.ts:

import { MongoClient } from "mongodb";
import { env } from "@/env/server.mjs";

const uri = env.MONGODB_URI;
const options = {};

if (!uri) {
  throw new Error('Invalid/Missing environment variable: "MONGODB_URI"');
}

let client;
let clientPromise: Promise<MongoClient>;

client = new MongoClient(uri, options);
clientPromise = client.connect();
export default clientPromise;

api/data/route.ts:

import clientPromise from "@/lib/mongodb";

export async function GET() {
  const client = await clientPromise;
  const db = client.db();

  const data = await db
    .collection("data")
    .find({})
    .toArray();

  return Response.json({
    type: "FeatureCollection",
    features: data,
  });
}

That works locally, but causes a failure within the CI environment, because I’ve not defined the MONGODB_URI environment variable:

$ next build
❌ Invalid environment variables: { MONGODB_URI: [ 'Required' ] }
 ⨯ Failed to load next.config.mjs, see more info here https://nextjs.org/docs/messages/next-config-error
> Build error occurred

I thought it would only be needed at runtime, not build. I can work around that by putting a dummy MONGODB_URI in .env.production, initially blank, but then looking like a dummy URL to satisfy the zod rule:

MONGODB_URI=mongodb+srv://host.placeholder.com/db

That works better, but still fails to build locally:

$ yarn run build
yarn run v1.22.19
$ next build
   ▲ Next.js 14.0.1
   - Environments: .env.production

 ✓ Creating an optimized production build
 ✓ Compiled successfully
 ✓ Linting and checking validity of types
   Collecting page data  ...Error: querySrv ENOTFOUND _mongodb._tcp.host.placeholder.com
    at QueryReqWrap.onresolve [as oncomplete] (node:internal/dns/promises:251:17) {
  errno: undefined,
  code: 'ENOTFOUND',
  syscall: 'querySrv',
  hostname: '_mongodb._tcp.host.placeholder.com'
}
 ✓ Collecting page data
   Generating static pages (0/7)  [=   ]
Error occurred prerendering page "/api/data". Read more: https://nextjs.org/docs/messages/prerender-error
Error: querySrv ENOTFOUND _mongodb._tcp.host.placeholder.com
    at QueryReqWrap.onresolve [as oncomplete] (node:internal/dns/promises:251:17)
 ✓ Generating static pages (7/7)

> Export encountered errors on following paths:
        /api/data/route: /api/data
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Why are what should be server-only dynamic elements being prerendered at build? Why is Mongo trying to make a connection at build time? (Presumably because of the pre-render). Why are my runtime secrets needed for build? It seems like they’re being baked in, and won’t be overridden by the actual environment.

I’d rather not expose these on my CI environment if I don’t have to. Do I really need to?

2

Answers


  1. Chosen as BEST ANSWER

    Saber BoukHriss's answer got me on the right track, particularly around debugging pre-rendering issues.

    Exporting a Route Segment Config from api/data/route.ts forces dynamic rather than pre-rendering of the API call. It's a bit of a blunt instrument so the other methods of triggering this (e.g. dynamic request parameters) may be better:

    api/data/route.ts:

    import clientPromise from "@/lib/mongodb";
    
    export const dynamic = "force-dynamic";
    
    export async function GET() {
    
      ...
    
    }
    

    However, attempts are still made to connect to the database as lib/mongodb.ts still runs at build. Following some hints in How to avoid executing page code during build?, I wrapped most of the file in a function. This is only then called if a page actually renders at build time, which it shouldn't if declared dynamic.

    lib/mongodb.ts:

    import { MongoClient } from "mongodb";
    import { env } from "@/env/server.mjs";
    
    export function getClientPromise() {
      const uri = env.MONGODB_URI;
      const options = {};
    
      if (!uri) {
        throw new Error('Invalid/Missing environment variable: "MONGODB_URI"');
      }
    
      let client;
      let clientPromise: Promise<MongoClient>;
    
      client = new MongoClient(uri, options);
      clientPromise = client.connect();
    
      return clientPromise;
    }
    
    export default getClientPromise;
    

    This still leaves env/server.js executing and validating at build time. This requires a valid but unused dummy MONGODB_URI in a .env file. createEnv has a parameter [skipValidation][4] which will prevent this. It comes with a warning that it is not encouraged and will lead to your types and runtime values being out of sync, but appears to be intended for build time.

    env/server.mjs:

    import { createEnv } from "@t3-oss/env-nextjs";
    import { z } from "zod";
    
    export const env = createEnv({
      server: {
        NODE_ENV: z.enum(["development", "test", "production"]),
        MONGODB_URI: z.string().url(),
      },
      runtimeEnv: process.env,
      // skip validation at build time as secrets will not be available
      skipValidation: true,
    });
    

  2. You’re encountering a few issues with how your Next.js application handles environment variables, particularly your MongoDB connection URI. Let’s address each of your concerns:

    • Runtime vs Build Time Environment Variables: In Next.js, environment variables can be loaded at build time or runtime, but the distinction is crucial. Build-time environment variables are embedded into the build and are the same for every application user. On the other hand, Runtime environment variables are read when the request is made, making them suitable for secrets like your MONGODB_URI.
    • MongoDB Connection During Build: The issue you’re encountering during the build process is likely because your MongoDB connection logic is not guarded against execution at build time. In Next.js, API routes and getServerSideProps functions are server-side and should not be invoked during the build. However, if your connection logic is imported into any part of your application that is part of the build process (like getStaticProps or any client-side code), it will attempt to establish a connection during the build.
    • Handling Environment Variables in CI/CD: It’s common practice to inject sensitive environment variables like MONGODB_URI in the CI/CD pipeline, ensuring they’re available during the build or runtime as required. However, if your application does not need the MONGODB_URI at build time (as it should be for sensitive data), you shouldn’t need to provide it in your CI environment.

    Solution suggestion:

    • Guard MongoDB Connection: Ensure your MongoDB connection logic is only invoked in server-side contexts (like API routes or getServerSideProps). This way, it won’t be called during the build process. You can use dynamic imports or conditional checks to ensure this.

    • Environment Variable Defaults: You can provide a default or dummy value for MONGODB_URI for your build environment that satisfies the Zod validation but does not establish an actual connection. This value should only be used in contexts without a real relationship.

    • Runtime Configuration: Ensure that your actual MONGODB_URI is provided as a runtime environment variable in your production environment. This can be achieved through your deployment configuration, ensuring it’s available when your application needs to connect to MongoDB.

    • Debugging Pre-rendering Issues: If you’re pre-rendering pages that make server-side calls (like MongoDB), ensure these calls are made in a context that’s not executed during the build process. For example, use getServerSideProps instead of getStaticProps for pages needing real-time server data.

    To conclude, your runtime secrets like MONGODB_URI should not be needed for the build unless they are inadvertently used in a build-time context. Adjust your application’s structure to ensure sensitive data is only accessed in server-side or runtime environments, and provide dummy values for build-time validations if necessary.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search