One command creates the project. The interesting part is what it generates, and one decision it does not make for you.
Creating a project
npx create-expo-app@latest my-app --template blank-typescript
cd my-app
npm installThat is the whole scaffold. There is no global CLI to install and nothing to keep up to date —
npx fetches the current version each time.
Expo Go versus a development build
This is the decision, and getting it wrong wastes an afternoon.
Expo Go is an app you install from the store. npx expo start serves
your JavaScript to it and you scan a QR code. Nothing is compiled, so it starts in seconds — and it
contains a fixed set of native modules chosen by Expo. Anything else cannot load.
A development build is your own app binary, containing exactly the native code your project declares:
npx expo run:ios
npx expo run:androidIt takes minutes the first time and seconds afterwards, and from then on it behaves like Expo Go — same fast refresh, same dev menu — except it can load your native dependencies.
The rule: the moment you add a library with native code, you need a development build. The demo app links Stripe's native SDK, so it does. If a tutorial tells you Expo cannot use native modules, it is describing Expo Go and it is years out of date.
What got generated
Less than you might expect, and deliberately so:
app/ your screens — the folder structure IS the navigation graph
assets/ icons and the splash image
app.json app configuration
package.json
tsconfig.jsonNote what is absent: no ios/ and no android/. Those are
generated on demand from your configuration by npx expo prebuild, which is what
run:ios calls. Treating them as build output rather than source is the single biggest
practical difference from a bare React Native project — upgrades stop being merge conflicts in
files nobody understands.
app.json, or app.config.ts
The scaffold gives you app.json. Rename it to app.config.ts the moment
you need a value from the environment, because JSON cannot read one:
const config: ExpoConfig = {
name: 'StayHub Pizza',
slug: 'pizza-react-native-mobile',
version: '1.0.0',
orientation: 'portrait',
icon: './assets/icon.png',
scheme: 'pizzaapp',
userInterfaceStyle: 'light',Typing it as ExpoConfig is worth doing on its own — Expo SDK 57 removed several keys
that older tutorials still use, and the compiler tells you immediately rather than the build failing
later.
scheme matters more than it looks: it is what lets a link open your app, and what
Stripe uses to return the customer after a 3D Secure redirect.
Environment values
Two mechanisms, and they are not interchangeable. Anything prefixed
EXPO_PUBLIC_ is inlined into the bundle and readable by anyone who downloads the app —
so it is for public values only, like a Stripe publishable key. Everything else goes
through extra in the config and is resolved at build time.
Neither is a secret store. A mobile bundle is downloadable and inspectable; anything that must stay secret lives on your server.
The scripts you will actually run
"start": "expo start",
"ios": "expo run:ios",
"android": "expo run:android",
"web": "expo start --web",
"prebuild": "expo prebuild",expo start --web renders the same components in a browser through
react-native-web. It is a genuinely useful preview and it is not the product —
several things do not work there, and later lessons name them as they come up.
Path aliases
Relative imports rot fast once you have a folder or two. TypeScript's paths fixes
it, and Metro reads the same file:
"paths": {
"@/*": ["./src/*"]
},So a screen imports @/features/cart/state/CartProvider rather than counting
../ segments. There is no second copy of this mapping to keep in sync — Expo's Metro
config picks it up from tsconfig.json directly.
What you need installed
Node 22. For iOS, Xcode 16.1 or newer — React Native 0.86 will not build on 15.x — plus an actual simulator runtime, which a fresh Xcode install often lacks. Check both:
xcodebuild -version
xcrun simctl list runtimesIf the second prints nothing, download a runtime in Xcode before you spend an hour reading a build error that does not mention it. For Android, Android Studio and one AVD.
Sanity-checking the project
npx expo-doctorIt catches version mismatches between Expo and its libraries, and missing native peer dependencies. That last one is worth the habit: a missing peer typically builds fine and then crashes on launch, which is a bad place to discover it.
What is next
Core Components — the parts you build
screens out of, and why there is no <div>.