Your First Mobile App
In this course, you will scaffold a mobile project with Grit, set up your development environment, run the app on a device or emulator, and understand how the Go API and Expo app work together. By the end, you will have a working mobile app connected to a live backend.
Reference docs: Your First Mobile App →
What is Mobile Development with Grit?
Grit scaffolds a monorepo that contains both a Go API and an Expo React Native app. You write React components that compile to native iOS and Android code. The Go API is exactly the same as in web projects — same routes, same services, same database. Only the frontend is different.
- • The Go API runs on your server — same as a web project
- • The Expo app runs on a phone (or emulator) and calls the API over HTTP
- • Shared types keep the API and app in sync
- • One codebase, two platforms (iOS + Android)
Prerequisites
You need the same tools as a web project, plus a few mobile-specific ones:
| Tool | What it does | Check |
|---|---|---|
| Go 1.21+ | Runs the backend API | go version |
| Node.js 18+ | Runs the Expo dev server | node --version |
| pnpm 8+ | Installs JavaScript packages | pnpm --version |
| Docker | Runs PostgreSQL, Redis, MinIO | docker --version |
| Expo Go app | Runs your app on a physical phone | App Store / Play Store |
Challenge: Install Expo Go
Install the Expo Go app on your phone from the App Store (iOS) or Play Store (Android). Open it and confirm it launches successfully.
Challenge: Check Your Tools
Open your terminal and run all four check commands from the table above. Confirm each tool is installed and returns a version number.
Scaffold a Mobile Project
Use the --mobile flag to scaffold a project with an Expo app instead of (or alongside) a web frontend:
grit new fitness --mobileThis creates a monorepo with the following structure:
fitness/
├── apps/
│ ├── api/ ← Go backend (same as web)
│ └── expo/ ← React Native app (Expo)
├── packages/
│ └── shared/ ← Shared types + schemas
├── docker-compose.yml
└── turbo.jsonThe apps/api/ directory is identical to a web project — same Go code, same routes, same services. The only difference is apps/expo/ replacingapps/web/.
app/ directory become navigable screens in your app.Challenge: Scaffold a Mobile Project
Run grit new fitness --mobile. Explore the folder structure. How many directories are inside apps/? What's inside packages/shared/?
Start the API
The API is a standard Grit Go backend. Start the infrastructure services first, then run the API server:
# Start PostgreSQL, Redis, MinIO
docker compose up -d
# Run the Go API
grit start serverThe API will start on http://localhost:8080. You can verify it's running by opening that URL in your browser — you should see a JSON health check response.
Challenge: Start the API
Start the infrastructure with docker compose up -d, then run the API. Open http://localhost:8080 in your browser. What JSON response do you see?
Start Expo
With the API running, open a new terminal and start the Expo dev server:
grit start expoExpo will display a QR code in the terminal. You have three options:
- Physical phone: Scan the QR code with Expo Go (Android) or your camera app (iOS)
- iOS Simulator: Press
iin the terminal (macOS only, requires Xcode) - Android Emulator: Press
ain the terminal (requires Android Studio)
Challenge: Run Expo
Start Expo and open the app on your phone or emulator. You should see the default home screen. Try editing apps/expo/app/index.tsx — does the change appear immediately?
Project Structure
The Expo app follows the same conventions as a Next.js project. Here's what's insideapps/expo/:
apps/expo/
├── app/ ← Screens (Expo Router)
│ ├── _layout.tsx ← Root layout (wraps all screens)
│ ├── index.tsx ← Home screen
│ ├── login.tsx ← Login screen
│ ├── register.tsx ← Register screen
│ └── (tabs)/ ← Tab navigation group
│ ├── _layout.tsx ← Tab bar configuration
│ ├── index.tsx ← Home tab
│ └── profile.tsx ← Profile tab
├── components/ ← Reusable UI components
├── hooks/ ← Custom hooks (useAuth, etc.)
├── lib/ ← API client, utilities
└── package.jsonKey Expo Router conventions:
- •
_layout.tsx— Wraps child screens. Defines navigation structure (stack, tabs). - •
index.tsx— The default screen for a directory (like/in a URL). - •
(tabs)/— A route group. The parentheses mean it doesn't appear in the URL. - • Any
.tsxfile inapp/becomes a screen automatically.
Challenge: Explore the Structure
Open apps/expo/app/. List all the files. Which file is the root layout? What does the (tabs)/ directory contain?
Challenge: Understand the Layout
Open apps/expo/app/_layout.tsx. What component does it use to wrap the app? Does it set up a Stack navigator or a Tab navigator at the root level?
Shared Types
The packages/shared/ directory is used by both the Go API and the Expo app. It contains Zod schemas and TypeScript types that keep the frontend and backend in sync.
packages/shared/
├── types/
│ ├── user.ts ← User type + schemas
│ ├── auth.ts ← Login/register schemas
│ └── api.ts ← API response types
├── schemas/
│ └── index.ts ← Zod validation schemas
└── constants/
└── routes.ts ← API route constantsWhen you use grit generate resource to add a new API resource, the TypeScript types are automatically generated in packages/shared/. Your mobile app gets them for free — no manual syncing required.
Challenge: Explore Shared Types
Open packages/shared/types/. What types are defined? Compare them to what the API returns at /api/auth/login. Are they the same?
The API Client
The mobile app calls the Go API using a configured HTTP client. Here's how a typical API call looks:
// lib/api.ts
const API_URL = "http://localhost:8080"
export async function login(email: string, password: string) {
const response = await fetch(API_URL + "/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
})
return response.json()
}localhost won't work — your phone can't reach your computer's localhost. You need to use your computer's local IP address instead (e.g., http://192.168.1.42:8080). Expo shows your local IP in the terminal output when you run grit start expo.http://10.0.2.2:8080 to reach the host machine's localhost. This is a special alias built into the Android emulator.Challenge: Find the API Client
Find the API client file in the Expo app (check lib/ or hooks/). What base URL does it use? What headers does it set?
Challenge: Test the Connection
With the API running, open the mobile app and try to register a new user. Check the API logs in your terminal — do you see the incoming request?
Summary
Here's what you learned in this course:
- Grit scaffolds a monorepo with a Go API and an Expo React Native app
- The Go API is identical to web projects — same code, same routes
- Expo Router provides file-based navigation (like Next.js for mobile)
- Shared types keep the API and mobile app in sync
- The API client connects the mobile app to the Go backend over HTTP
Challenge: Register a User
Using the mobile app, register a new user with an email and password. Then log in with those credentials. Does the app navigate to the home screen?
Challenge: End-to-End Test
Create a blog post via the API docs at http://localhost:8080/swagger. Then open the mobile app and navigate to the content screen. Does the new post appear? Pull down to refresh if needed.
Enjoying the course?
Help us grow, star us on GitHub, subscribe on YouTube, and follow on LinkedIn.
