skip to content

In a React Native project's metro.config.js, what do the resolver, transformer, serializer and server sections each control?

level: juniorimportance: should knowfreq 42%

answer

  1. one section per bundling stage
  2. resolver finds files, transformer compiles them
  3. serializer assembles the bundle
  4. server: the dev server, port 8081
  5. top level: projectRoot, watchFolders, maxWorkers

basics

~20 s

Metro's config mirrors its pipeline: resolver decides which file an import points to, transformer compiles each file, serializer assembles modules into the bundle, and server configures the development server that serves it, on port 8081 by default.

solid answer

~40 s

A Metro config is organised around the bundling stages. `resolver` controls how an import specifier becomes a file: extensions, platforms, `node_modules` lookup, custom resolution. `transformer` controls how each file is compiled: the Babel transformer, `getTransformOptions`, the minifier. `serializer` controls how transformed modules become a bundle: polyfills, modules run before the entry, module ids. `server` configures the development server that `npx react-native start` or `npx expo start` runs, with `port` defaulting to 8081. Around them sit top-level options such as `projectRoot`, `watchFolders`, `maxWorkers`, `cacheStores` and `resetCache`, plus `watcher` and `symbolicator`. Knowing the sections tells you where a fix belongs: a new file type is a resolver change, a new compile step is a transformer change, a port clash is a server change.

code

javascript · 12 lines
javascript
// metro.config.js (React Native 0.87 template shape)
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');

/** @type {import('metro-config').MetroConfig} */
const config = {
  resolver: {},    // import specifier -> file
  transformer: {}, // compile each file
  serializer: {},  // assemble the bundle
  server: {},      // development server, port 8081 by default
};

module.exports = mergeConfig(getDefaultConfig(__dirname), config);

go deeper

for a junior

Map each section to its stage: resolver finds files, transformer compiles them, serializer builds the bundle, server runs the dev server on 8081.

for a middle

Explain which options reach release bundles and which only affect development, and name the key a common fix touches in each section.

for a senior

Triage bundling problems by stage first, so a fix lands in the right section instead of piling unrelated overrides into one config.

for a principal

Keep the Metro config small and owned: every override of a framework default is a future upgrade risk that someone must justify.

## Why the config is shaped like the pipeline **Metro** is the bundler React Native and Expo use for JavaScript and assets. It works in three stages: **resolution** (which file does this import mean?), **transformation** (compile that file) and **serialization** (put the compiled modules into a bundle). Its configuration file, `metro.config.js` in most projects, follows the same shape: one section per stage, plus a section for the development server and a few top-level options. Once you know the stages, you know where to look. A React Native template project starts from `getDefaultConfig(__dirname)` exported by `@react-native/metro-config` and merges its own changes on top; an Expo project starts from `getDefaultConfig` in `expo/metro-config`. ## The sections | Section | Controls | Typical keys | |---|---|---| | top level | the project and the process | `projectRoot`, `watchFolders`, `maxWorkers`, `cacheStores`, `resetCache`, `transformerPath` | | `resolver` | turning an import specifier into a file | `sourceExts`, `assetExts`, `platforms`, `blockList`, `resolveRequest`, `nodeModulesPaths` | | `transformer` | compiling each file | `babelTransformerPath`, `getTransformOptions`, `minifierPath`, `assetRegistryPath` | | `serializer` | assembling modules into the bundle | `getPolyfills`, `getModulesRunBeforeMainModule`, `createModuleIdFactory`, `isThirdPartyModule` | | `server` | the development server | `port`, `rewriteRequestUrl`, `tls` | | `watcher` | noticing file changes | `additionalExts`, `healthCheck`, `watchman` | | `symbolicator` | cleaning up stack traces | `customizeFrame` | A few of these deserve a sentence each: - **`projectRoot`** is the root folder of the app. Files outside it must be reachable through `watchFolders`. - **`maxWorkers`** sets how many workers transform files in parallel; its default is derived from the machine's core count. - **`serializer`** is rarely touched in an app. React Native uses it to run its environment setup module before your entry file and to add its polyfills. - **`server.port`** defaults to 8081. Both React Native's and Expo's defaults read the `RCT_METRO_PORT` environment variable first. ## The development server `npx react-native start` and `npx expo start` both start Metro's **development server**. It keeps the module graph in memory, watches the file system, and answers HTTP requests from the app for bundles, assets and source maps, pushing updates to the running app when files change. A release build does not use the server: the native build runs Metro once in bundling mode and embeds the resulting file in the app. That is why a `server` change only affects development, while `resolver`, `transformer` and `serializer` changes affect release bundles too. ## Where a fix goes 1. A new file type fails to import: **resolver** (its extension is missing from `assetExts` or `sourceExts`). 2. A file type needs compiling differently, such as SVG as components: **transformer**. 3. Two projects fight over port 8081: **server** (`port`, or the `--port` CLI flag). 4. Something must run before the app's entry module: **serializer**. 5. A package outside the project folder is not found or not watched: top-level **`watchFolders`**, or the resolver's lookup paths. 6. The machine runs out of memory while bundling: top-level **`maxWorkers`**. ## How defaults, the file and CLI flags combine The effective configuration is built in layers. Metro's own defaults come first; the framework package (`@react-native/metro-config` or `expo/metro-config`) adds its layer; your `metro.config.js` is merged over that; and finally a few command-line flags win over everything: `--port`, `--max-workers`, `--reset-cache`, `--projectRoot`, `--watchFolders` and `--config` on `npx react-native start`. That is why a value can differ between two teammates who run Metro with different flags, and why a flag is the quickest way to test a change before committing it to the file. ## Common misunderstandings - The config does not describe the app itself: the app's name, icons and permissions live in native projects or in Expo's app config, not in Metro's. - Metro options are not shared with Jest. Jest has its own configuration and its own transformer. - Each section is merged separately when you extend the defaults, so changing one key does not reset the others in that section, but assigning an array replaces the whole array. A junior answer that maps each section to its stage and can say where two or three common fixes go is exactly what an interviewer wants.

  • Two React Native projects both try to start Metro on port 8081. What are your options?
    Start one on another port with `--port 8082` on `npx react-native start` or `npx expo start`, set `server.port` in its Metro config, or set `RCT_METRO_PORT`, which both default configs read. The native app must also be pointed at the new port, otherwise it keeps asking 8081 for its bundle.
  • Does a change in Metro's server section affect a release build?
    No. Release builds run Metro once in bundling mode and embed the result in the app, so no development server is involved. Changes to the resolver, transformer and serializer do affect release bundles.

A kitchen line: the resolver fetches the right ingredient from the store room, the transformer cooks each ingredient separately, the serializer plates the dish, and the server is the pass where plates go out to the dining room while service is running.

saying these in an interview costs you the question

  • metro.config.js is where the app's name, icon and permissions are set
  • The server section changes how the release bundle is built
  • Jest reads the same Metro config to transform test files
  • Adding a file extension belongs in the transformer section
  • Metro's development server port is fixed at 8081 and cannot change