Every Expo project I've regretted had the same origin story: npx create-expo-app, straight into building screens, and "I'll set up the boring stuff later." Later arrived as a broken production build, secrets in the bundle, a crash nobody could reproduce, and a weekend of retrofitting.

The boring stuff takes about an hour on day one. On day ninety it takes a week. This is the checklist I now run before writing a single feature, in order, with the commands. At the end you have your own boilerplate: a repo you can copy for every new app.

0. Start from the current template (2 minutes)

npx create-expo-app@latest my-app cd my-app npx expo-doctor 

The default template already gives you TypeScript, Expo Router, and an @/* path alias. Run expo-doctor immediately and fix anything it flags while the project is still empty.

1. Make TypeScript strict (5 minutes)

In tsconfig.json:

{ "extends": "expo/tsconfig.base", "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "paths": { "@/*": ["./*"] } } } 

Add a script so CI and you run the same check:

{ "scripts": { "typecheck": "tsc --noEmit" } } 

Turning strict mode on later means fixing hundreds of errors at once. Turning it on now means fixing zero.

2. Linting and formatting (5 minutes)

npx expo lint # sets up ESLint with Expo's config on first run npm i -D prettier eslint-config-prettier 

Add the rules that catch real bugs, not style debates:

  • react-hooks/exhaustive-deps as an error
  • @typescript-eslint/no-explicit-any as an error
  • no-restricted-imports for @react-navigation/*, so navigation always comes from expo-router

3. Decide the folder structure before it decides itself (5 minutes)

app/ # routes only (Expo Router) components/ # reusable UI features/<name>/ # screens' logic, hooks, and components per feature lib/ # clients (supabase, api), utilities queries/ # data-fetching hooks theme/ # colors, spacing, typography tokens 

The rule that matters most: app/ contains routes, not logic. A route file imports a screen from features/ and renders it. Routes stay thin, and features stay testable.

4. Theme tokens before the first screen (5 minutes)

// theme/tokens.ts export const colors = { bg: '#0B0B0F', surface: '#16161D', text: '#F5F5F7', muted: '#9A9AA3', primary: '#3B82F6', danger: '#EF4444', } as const; export const space = { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 } as const; export const radius = { sm: 6, md: 12, lg: 20 } as const; 

Whether you use NativeWind, StyleSheet, or a UI kit, the tokens live in one place. When the designer changes the primary color, it's one line, not a search across forty files.

5. Data fetching that handles loading and errors (5 minutes)

npm i @tanstack/react-query 
// app/_layout.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Stack } from 'expo-router'; const queryClient = new QueryClient(); export default function RootLayout() { return ( <QueryClientProvider client={queryClient}> <Stack /> </QueryClientProvider> ); } 

Then make it a rule: no fetch or database calls inside useEffect in screens. Every request goes through a hook in queries/, which gives you caching, retries, and loading and error states for free.

6. Secure storage for anything sensitive (3 minutes)

npx expo install expo-secure-store 

Session tokens and refresh tokens go in the Keychain (iOS) or Keystore (Android) via expo-secure-store, not in AsyncStorage. Set the pattern now and every auth library you add later plugs into it.

7. Environment variables, the safe way (5 minutes)

Two kinds of values, two places:

  • Public config (API URL, publishable keys): EXPO_PUBLIC_ variables, which are inlined into the JavaScript bundle.
  • Secrets (third-party API keys, service keys): never in the app. They live on your server or in a serverless function the app calls.

Store build-time values in EAS so every machine and CI run uses the same ones:

npm i -g eas-cli eas init eas env:create --environment production --name EXPO_PUBLIC_API_URL --value https://api.example.com eas env:pull --environment development 

Add a CI check that fails if anything like EXPO_PUBLIC_.*SECRET appears in the repo.

8. Build profiles and a development build (10 minutes)

npx expo install expo-dev-client eas build:configure 

Set up three profiles in eas.json, each linked to its environment:

{ "cli": { "appVersionSource": "remote" }, "build": { "development": { "developmentClient": true, "distribution": "internal", "environment": "development" }, "preview": { "distribution": "internal", "environment": "preview", "channel": "preview" }, "production": { "environment": "production", "channel": "production", "autoIncrement": true } } } 

Then make your first development build:

eas build:dev --platform ios 

From here on, you develop in your own dev client instead of Expo Go. Push notifications, payments, and most native modules don't work in Expo Go, so finding out on day one beats finding out the week before launch.

9. Over-the-air updates, configured safely (5 minutes)

npx expo install expo-updates eas update:configure 

In app.json, use the fingerprint runtime policy so an update only reaches binaries that can run it:

{ "expo": { "runtimeVersion": { "policy": "fingerprint" } } } 

Now a JavaScript-only bug fix is a five-minute publish instead of a store review.

10. Crash reporting before the first user (5 minutes)

npx @sentry/wizard@latest -i reactNative 

The wizard adds the SDK, the config plugin, and source map uploads during EAS builds. A crash report with a readable stack trace is the difference between "fixed in an hour" and "can't reproduce."

11. CI that runs on every pull request (5 minutes)

# .github/workflows/ci.yml name: CI on: [pull_request] jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 22, cache: npm } - run: npm ci - run: npm run typecheck - run: npx expo lint - run: npx expo install --check 

Three checks: types, lint, and dependency versions that match your Expo SDK. That last one catches the "works locally, fails on EAS" class of bugs before they cost a build.

12. The two screens every app needs (optional, but cheap now)

  • Settings → Account → Delete account. Both app stores require in-app account deletion if users can sign up. Add the route now, even if the button is a stub.
  • Error and empty states. A shared <ErrorState /> and <EmptyState /> component means every screen handles failure the same way.

Save it as your boilerplate

Once all twelve are done, commit the repo, tag it, and mark it as a GitHub template repository. Every new app starts with:

gh repo create my-next-app --template you/expo-boilerplate --private --clone 

Update the template whenever you upgrade the Expo SDK, and every future project starts current.

The checklist

  • [ ] Fresh template, expo-doctor clean
  • [ ] Strict TypeScript and a typecheck script
  • [ ] ESLint and Prettier with bug-catching rules
  • [ ] Folder structure: routes in app/, logic in features/
  • [ ] Theme tokens in one file
  • [ ] TanStack Query for all data fetching
  • [ ] expo-secure-store for tokens
  • [ ] Public vs secret env vars separated; values on EAS
  • [ ] Three build profiles and a development build
  • [ ] expo-updates with fingerprint runtime version
  • [ ] Sentry with source maps
  • [ ] CI: typecheck, lint, dependency check
  • [ ] Account deletion route and shared error/empty states

About an hour of work, and every app you build afterward starts from a foundation instead of a to-do list.

If you'd rather skip straight to screens, RapidNative generates Expo apps as real React Native code you own, so you can drop them into a setup like this one and keep going.

What's on your day-one list that isn't on mine?