skip to content

Workflows & Prebuild

How an Expo project is generated and run: Continuous Native Generation, Expo Go versus development builds, and app config. Interviewers ask when you leave Expo Go and what replaced ejecting.

part ofExpo (React Native)overview, primer and where to startread it →
on this pageshow

explore

questions

18

In an Expo project, what is the difference between a static app.json and a dynamic app.config.ts, and when do you need the dynamic one?

level: juniorimportance: must knowfreq 52%

answer

  1. JSON file vs code run by Node
  2. static read first, then passed in
  3. ({ config }) => ({ ...config })
  4. TS file wins over JS file
  5. no Promises, serialized to JSON

basics

~20 s

app.json is static JSON that CLI tools can also edit; app.config.ts is code Expo CLI evaluates in Node, so it can read environment variables and compute values. When both exist, app.json is read first and handed to the dynamic config.

solid answer

~40 s

The app config describes the app: name, slug, version, icons, `ios.bundleIdentifier`, `android.package`, `extra` and more. It drives `npx expo prebuild`, how Expo Go and dev builds load the project, and the update manifest. `app.json` is plain JSON, so CLI tools can write into it automatically. `app.config.ts` (or `.js`) is evaluated by Expo CLI in Node, so it can branch on `process.env`, share constants and compute values. If it exports a function, it receives the normalized static config as `({ config })` and returns the final object, like middleware. You need the dynamic form when a value must differ per environment or be computed; everything static can stay in `app.json`. The result must resolve synchronously and is serialized to JSON before any tool uses it; `npx expo config` prints it.

code

typescript · 8 lines
typescript
import { ExpoConfig, ConfigContext } from 'expo/config';

export default ({ config }: ConfigContext): ExpoConfig => ({
  ...config,
  name: config.name ?? 'Car Share',
  slug: 'car-share',
  version: process.env.APP_VERSION ?? '1.0.0',
});

go deeper

for a junior

Know that app.json is static JSON and app.config.ts is code that runs in Node and can read process.env.

for a middle

Explain the resolution order: static read first, passed as config to an exported function, TypeScript preferred over JavaScript, no Promises, serialized result.

for a senior

Keep stable values static so tools can edit them, keep the dynamic layer thin, and verify the resolved output with npx expo config rather than reasoning about it.

for a principal

Treat the app config as the source of generated native projects; its structure decides how easily variants, white labels and upgrades stay reproducible.

## What the app config is Every Expo project has an **app config** at its root, next to `package.json`. It is the single description of the app that Expo's tooling reads: - **Prebuild** (Continuous Native Generation) uses it to generate the `ios/` and `android/` projects: display name, `ios.bundleIdentifier`, `android.package`, icons, splash screen, URL scheme. - **Expo Go and development builds** read it through the manifest the dev server serves. - **EAS Update** embeds the public part of it in each update's manifest. - **Your JavaScript** can read the public part at runtime with `Constants.expoConfig` from `expo-constants`. It comes in two flavours: **static** (`app.json` or `app.config.json`) and **dynamic** (`app.config.js` or `app.config.ts`). ## Static config: app.json `app.json` is plain JSON. Its strengths are the strengths of data: - **Tools can edit it.** Static configs can be updated automatically by CLI tools; a dynamic config is code, so the developer must edit it by hand. - **It is obvious.** Anyone can read the whole config without running anything. - **The `expo` wrapper.** If the file has a top-level `expo: {}` object, that object is used in place of the root and other keys are ignored, which is why most `app.json` files nest everything under `expo`. Its weakness is the same: JSON cannot read an environment variable, compute a string, or branch. ## Dynamic config: app.config.js and app.config.ts A dynamic config is a module that **Expo CLI evaluates in Node** each time it needs the config, for example when `npx expo start` serves a manifest or when prebuild runs. That buys you: - comments, variables, helper functions and shared constants; - `process.env` access, so values can differ per environment (Expo CLI has already loaded the project's `.env` files into the process at that point); - TypeScript types via `ExpoConfig` and `ConfigContext` from `expo/config` when you use `app.config.ts`. It also has limits: it **cannot return a Promise** (no fetching remote settings during resolution), and its output must be plain data, because it is **serialized to a JSON manifest** before any tool uses it. `app.config.ts` works out of the box, but importing other TypeScript files from it needs the `tsx` require hook, which the Expo TypeScript guide describes. ## The resolution rules Expo CLI resolves the final config in a fixed order: 1. Read the **static** config: `app.config.json` if it exists, otherwise `app.json`. With neither, defaults are inferred from `package.json` and dependencies. 2. Read the **dynamic** config if `app.config.ts` or `app.config.js` exists. **If both exist, the TypeScript file is used.** 3. If the dynamic config **exports a function**, it is called with the static config as `({ config })`, like middleware; the usual pattern spreads `...config` and overrides a few keys. 4. The **return value** is the final config; it cannot contain Promises. 5. Everything is evaluated and **serialized** before tools consume it. 6. A top-level `expo` object in the result replaces the root. ## Choosing between them | Need | app.json only | app.json + app.config.ts | |---|---|---| | Fixed name, icon, version | enough | not needed | | CLI tools editing the config for you | yes | only the static half | | Different bundle ID or API URL per environment | no | yes | | Values read from `process.env` at config time | no | yes | | Typed config with autocomplete | schema only | `ExpoConfig` type | A common, tidy split is to keep stable values in `app.json` and let a small `app.config.ts` override only what varies, so tools can still edit the static part. ## Common mistakes - **Two dynamic files.** Adding `app.config.ts` next to an old `app.config.js` silently retires the JavaScript one, because the TypeScript file wins. - **Forgetting to spread.** A function that returns a fresh object instead of `{ ...config, ... }` drops everything `app.json` defined, from icons to `ios` settings. - **Shallow overrides of nested keys.** Returning `ios: { bundleIdentifier: '...' }` replaces the whole `ios` object; spread `...config.ios` first. - **Side effects.** The config runs every time a tool resolves it, so it must stay fast and deterministic: no network calls, no writing files. - **Expecting runtime evaluation.** The config is evaluated on the developer's or build machine, never on the phone; the app only sees the serialized public result. ## Seeing what you actually get Because the final config is computed, never guess it. `npx expo config` prints the resolved config Expo CLI will use, and `npx expo config --type public` prints the part that is embedded in builds and updates and visible through `Constants.expoConfig`. Also avoid importing `app.json` into app code: that bundles the raw file, not the resolved result a dynamic config produced.

  • An Expo project has app.config.js and someone adds app.config.ts beside it; what changes?
    Expo CLI now evaluates only `app.config.ts`, because the TypeScript dynamic config takes precedence when both exist. Values set only in `app.config.js` silently disappear from the resolved config. Delete one of the two files and confirm the result with `npx expo config`.
  • Why does keeping an app.json alongside app.config.ts still help?
    Static config can be updated automatically by CLI tools, while a dynamic config must be edited by hand. Keeping stable values in `app.json` and having `app.config.ts` spread `...config` and override only the varying keys keeps the tool-editable part editable and the code small.

saying these in an interview costs you the question

  • A dynamic config cannot see what app.json already defines
  • The dynamic config can await a fetch of remote settings
  • When both JS and TS configs exist, they are merged
  • Importing app.json in app code gives the resolved config
  • The app config is read by the app at runtime straight from disk
open as a page

In an Expo project, what does npx expo prebuild do, and why are the ios and android folders usually left out of git?

level: juniorimportance: must knowfreq 58%

basics

~20 s

npx expo prebuild generates the ios and android native projects from the SDK's template, the app config and its config plugins, plus autolinked modules. Because they can be regenerated at any time, they are treated as build output and gitignored.

open as a page

In Expo, what is the difference between Expo Go and a development build, and when does a project outgrow Expo Go?

level: juniorimportance: must knowfreq 68%

basics

~20 s

Expo Go is a prebuilt app with a fixed set of native libraries for one SDK. A development build is your own debug binary with expo-dev-client, holding your native code and config. Outgrow Expo Go when you need native code it lacks.

open as a page

How do you start a new Expo project with create-expo-app, and how do its default, blank, tabs and bare-minimum templates differ?

level: juniorimportance: must knowfreq 45%

basics

~10 s

Run npx create-expo-app with an optional --template: default adds Expo Router and TypeScript for multi-screen apps, blank is minimal, tabs sets up file-based tab navigation, and bare-minimum also generates the android and ios folders.

open as a page

In an Expo project, how do EXPO_PUBLIC_ variables from .env files get into the app, and what limits does build-time inlining impose?

level: middleimportance: must knowfreq 58%

basics

~20 s

Expo CLI loads .env files, and Metro replaces each static process.env.EXPO_PUBLIC_NAME reference in your code with its literal value when it builds a release bundle. In a release bundle the value is frozen as plain text until the next build or update.

open as a page

In an Expo project, what do npx expo run:ios and npx expo run:android do, and when do you need to run them again?

level: middleimportance: must knowfreq 50%

basics

~20 s

They compile the native app locally with Xcode or Gradle, install it on a simulator, emulator or device, and start Metro. Run them again only after native changes: a native library, a config plugin, or native app config.

open as a page

In an Expo app, how do you read app config values at runtime through Constants.expoConfig and extra, and how does that differ from EXPO_PUBLIC_ variables?

level: middleimportance: should knowfreq 40%

basics

~20 s

Import Constants from expo-constants and read Constants.expoConfig?.extra, which holds the public part of the resolved app config. EXPO_PUBLIC_ variables are literals inlined into your code; extra is data carried in the app's embedded config or manifest.

open as a page

Since Expo SDK 57, what does npx expo prebuild do to existing native folders by default, and when would you pass --no-clean?

level: middleimportance: should knowfreq 38%

basics

~20 s

Since SDK 57, npx expo prebuild deletes the existing ios and android folders it targets and regenerates them. Pass --no-clean only for a quick re-sync you accept may differ from a fresh generation, such as testing a plugin change.

open as a page

How does upgrading the Expo SDK differ between a project using Continuous Native Generation and one that commits its native folders?

level: middleimportance: should knowfreq 42%

basics

~20 s

With CNG you bump expo and its packages, then regenerate the native folders from the new SDK's template. With committed native folders you must apply the native template changes yourself, using Expo's native upgrade helper, then reinstall pods.

open as a page

In an Expo project, what does expo-dev-client add to a debug build, and how does that development build find the Metro dev server?

level: middleimportance: should knowfreq 38%

basics

~20 s

expo-dev-client adds a launcher screen, an extended dev menu and the ability to load published updates. The build finds Metro through the launcher's list of dev servers on the network, or through a QR code deep link carrying the server URL.

open as a page

An Expo app's push-notification feature fails in Expo Go on Android and only warns on iOS; why, and what do you build instead?

level: middleimportance: should knowfreq 45%

basics

~20 s

Remote push depends on the app's own push credentials, which Expo Go cannot carry, and Android push was removed from Expo Go in SDK 53, so expo-notifications throws there. Build a development build with expo-dev-client and test push in it.

open as a page

Why do current Expo projects run npx expo start from the local CLI inside the expo package instead of a globally installed expo-cli?

level: middleimportance: should knowfreq 42%

basics

~20 s

The Expo CLI now ships inside each project's expo package, so npx expo start always runs the CLI version that matches that project's SDK. The old global expo-cli is deprecated, and its retired commands point to replacements.

open as a page

In an Expo car-sharing app, how would you use app.config.ts with an APP_VARIANT switch so dev, staging and production install side by side with their own bundle IDs and API URLs?

level: seniorimportance: should knowfreq 38%

basics

~10 s

Branch in app.config.ts on process.env.APP_VARIANT to set name, ios.bundleIdentifier, android.package and the API URL per variant. Set APP_VARIANT everywhere the config is evaluated: native builds, npx expo start and update exports.

open as a page

Your managed Expo habit-tracking app needs a native camera SDK with iOS and Android permission entries; how do you add it without committing ios and android?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Install the library with npx expo install, add its config plugin and permission strings to the app config, then regenerate with npx expo prebuild or let EAS Build do it, and ship a new native build. The native folders stay generated.

open as a page

After a teammate adds a native library, your Expo development build fails with "Cannot find native module"; how do you diagnose it and stop it recurring?

level: seniorimportance: should knowfreq 34%

basics

~20 s

The installed development build predates the library, so its JavaScript asks for native code the binary lacks. Rebuild the development build, and stop recurrences by detecting native changes, for example with a fingerprint check that says when a new build is needed.

open as a page

Before releasing an Expo app, how would you use npx expo-doctor, and how do you act on its dependency, React Native Directory and app-config-sync warnings?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Run npx expo-doctor after upgrades, after adding libraries and before a release. Fix version mismatches with npx expo install --fix, review packages flagged by the React Native Directory check, and resolve config-sync warnings about committed native folders.

open as a page

An Expo team wants to commit ios and android and edit them directly instead of using Continuous Native Generation; how do you decide, and what does it cost?

level: principalimportance: should knowfreq 30%

basics

~20 s

Commit native folders only when needed native changes cannot reasonably be expressed as packages and config plugins. The cost is owning both native projects: manual SDK upgrades, app config native fields no longer applied, and drift nobody regenerates away.

open as a page

What is Expo Snack, and when would you use it instead of a local Expo project?

level: juniorimportance: nice to knowfreq 20%

basics

~20 s

Expo Snack is an in-browser environment for writing and running Expo code with nothing installed, previewing on Android, iOS or web. It suits prototypes, shared snippets and bug reproductions, not apps that need custom native code.

open as a page