Sebastian Gomez
Customizing the configuration in Next.js
Next.js makes it remarkably easy to modify the behavior of its execution flow. The most common case, for example, is adding environment variables to your project. In this article we'll show you how to do it and how to extend Next.js through different phases and plugins.
Valid as of Next.js 16: the concept of customizing next.config.js is still fully valid. What changed since 2022 are some API details. In this revision we use next.config.mjs (ESM), fix the handling of webpack, and start from the built in support for .env files.
Adding environment variables
In Next.js, the recommended way to handle environment variables is not to touch next.config.mjs but to use .env files. Next.js loads them automatically as of version 9.4, so you usually don't need to configure anything.
Create a .env.local file at the root of your project and add your variables:
# .env.local
MY_ENV_VAR=HOLA
NEXT_PUBLIC_APP_URL=https://www.sebastian-gomez.comImportant tip: variables you want to read in the browser must be prefixed with NEXT_PUBLIC_. Without that prefix, the variable only exists on the server and arrives as undefined on the client. And never put secrets in NEXT_PUBLIC_ variables, because they end up exposed in the client bundle. Also remember to keep your .env.local out of version control; never commit it to GitHub.
To read the variable in any page or component, just use process.env:
// The variable is prefixed with NEXT_PUBLIC_ so it's available on the client
<a href={process.env.NEXT_PUBLIC_APP_URL}>Go to the app</a>The next.config.mjs file also has an env key, which still works in Next.js 16, but today it's considered a legacy option: it only inlines values at build time. For most cases, prefer .env.local.
// next.config.mjs — the 'env' key still works, but prefer .env.local today
/** @type {import('next').NextConfig} */
const nextConfig = {
// These variables are inlined at build time
env: {
MY_ENV_VAR: "HOLA",
OTHER_ENV_VAR: "NO PONGAS SECRETOS AQUI",
},
};
export default nextConfig;Exporting the configuration through a function
next.config.mjs can also export a function. This lets us, for example, return a different configuration depending on the build phase Next.js is in:
// Import the 'PHASE_DEVELOPMENT_SERVER' constant from next/constants
import { PHASE_DEVELOPMENT_SERVER } from "next/constants.js";
// Export a function that receives the current phase and the default Next.js config
export default (phase, { defaultConfig }) => {
// If we're in the development phase, we add an environment variable
if (phase === PHASE_DEVELOPMENT_SERVER) {
return {
...defaultConfig,
env: {
// In development mode we set 'IS_THIS_A_FUNCTION' to 'TRUE'
IS_THIS_A_FUNCTION: "TRUE",
},
};
}
// If we're not in development, return the default config
return defaultConfig;
};Hooking into Next.js phases
Next.js ships many constants that let us hook into certain phases, such as development, production, or build. A typical case is monitoring the bundle size during compilation.
There's an important detail worth correcting from the original 2022 approach. Bundle analysis happens when the app is built, so the correct phase is PHASE_PRODUCTION_BUILD, not PHASE_PRODUCTION_SERVER (the latter runs while serving, when the bundle already exists). On top of that, in next.config.mjs the webpack key is a function that receives the config and must return it, not an object. Assigning a plain object does nothing.
// Import the 'PHASE_PRODUCTION_BUILD' constant from next/constants
import { PHASE_PRODUCTION_BUILD } from "next/constants.js";
import pkg from "webpack-bundle-analyzer";
const { BundleAnalyzerPlugin } = pkg;
export default (phase, { defaultConfig }) => {
// Only in the production build phase do we add the plugin
if (phase === PHASE_PRODUCTION_BUILD) {
return {
...defaultConfig,
// webpack is a FUNCTION (config) => config, not an object.
// We mutate the received config and return it.
webpack: (config) => {
config.plugins.push(new BundleAnalyzerPlugin());
return config;
},
};
}
// In any other phase, return the default config
return defaultConfig;
};Note: Next.js 16 uses Turbopack by default, and webpack based bundle analysis only applies when you build with webpack. The idiomatic, maintained approach today is to use the official @next/bundle-analyzer wrapper, which handles everything for you:
// next.config.mjs with the maintained @next/bundle-analyzer wrapper
import bundleAnalyzer from "@next/bundle-analyzer";
const withBundleAnalyzer = bundleAnalyzer({
// Enabled only when ANALYZE=true
enabled: process.env.ANALYZE === "true",
});
/** @type {import('next').NextConfig} */
const nextConfig = {
// Your Next.js configuration
};
export default withBundleAnalyzer(nextConfig);In the `next/constants` documentation you can see all the different phases Next.js has and all their constants.
Extending the configuration for other file formats
If you're wondering how to make your pages not necessarily JavaScript but also markdown (.md) or extended markdown (.mdx) files, Next.js offers the official @next/mdx package.
There's an important change since 2022. With Turbopack (the default bundler in Next.js 16), remarkPlugins and rehypePlugins must be passed as serializable references, that is, the package name as a string or a [string, options] tuple, instead of imported function instances. That's why we use createMDX like this:
// next.config.mjs
import createMDX from "@next/mdx";
const withMDX = createMDX({
options: {
// Plugins as serializable (string) references, not imported instances.
// Under Turbopack this is required.
remarkPlugins: [["remark-gfm"], ["remark-emoji"]],
rehypePlugins: [],
},
});
/** @type {import('next').NextConfig} */
const nextConfig = {
// We tell Next.js which extensions should be treated as pages
pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
};
export default withMDX(nextConfig);Note: review the list of remark/rehype plugins you actually need. The old remark-images is essentially unmaintained, so here we replace it with remark-gfm, which is the most common one today. Adjust to your case.
In our post on Plugins in Next.js we go deeper into the plugins you can use and how to combine them.
Conclusions
The original Next.js configuration can be modified very easily, and you can extend its functionality as much as you want.
Don't be afraid to do it, because it will let you get much more out of Next.js.
Knowing the different phases and constants in Next.js will help you customize your projects even further.
Exercises to practice
- Create a basic Next.js 16 project and add environment variables using a
.env.localfile, exposing one to the client with theNEXT_PUBLIC_prefix. - Hook into different project phases using the
next/constantsconstants, for example adding bundle analysis only in the build phase. - Extend your project's configuration to support markdown (.md) and extended markdown (.mdx) files with
@next/mdx.
3-point summary
- Handle environment variables with the built in
.envsupport; use theNEXT_PUBLIC_prefix for the ones you need in the browser, and reserve theenvkey only for build time inlining. - Hook into Next.js phases with the
next/constantsconstants, remembering thatwebpackis a(config) => configfunction and that bundle analysis belongs in the build phase (or, even better, use@next/bundle-analyzer). - Extend the configuration to support other formats like markdown with
createMDX, passing plugins as serializable references under Turbopack.
That's all, I hope this post is useful to you and that you can apply it to a project you have in mind, or that it simply helped you understand how modifying the configuration of your Next.js projects works.
Leave me a comment if it helped, if you want to add an opinion, or if you have any questions, don't hesitate to write below. And remember, if you liked it, you can also share it using the social links below.
Sebastian Gomez
Creador de contenido principalmente acerca de tecnología.