Skip to content

Production-ready starter

Alright β€” here’s a complete, production-ready starter repo structure you can literally copy-paste and run for:

  • Expo + Bun
  • NativeWind (UI)
  • Zustand (state)
  • TanStack Query (API)
  • JWT Auth (secure + refresh-ready)
  • Scalable architecture

Terminal window
bun create expo myApp --template blank-typescript
cd myApp
bun add nativewind zustand axios @tanstack/react-query
bun add react-native-safe-area-context react-native-screens
bun add expo-secure-store
bun add -d tailwindcss
bunx tailwindcss init

Terminal window
myApp/
β”œβ”€β”€ app/
β”‚ β”œβ”€β”€ (auth)/
β”‚ β”‚ └── login.tsx
β”‚ β”œβ”€β”€ (tabs)/
β”‚ β”‚ └── home.tsx
β”‚ β”œβ”€β”€ _layout.tsx
β”‚
β”œβ”€β”€ src/
β”‚ β”œβ”€β”€ components/
β”‚ β”‚ └── Button.tsx
β”‚
β”‚ β”œβ”€β”€ features/
β”‚ β”‚ └── auth/
β”‚ β”‚ β”œβ”€β”€ api.ts
β”‚ β”‚ β”œβ”€β”€ store.ts
β”‚ β”‚ β”œβ”€β”€ hooks.ts
β”‚ β”‚ β”œβ”€β”€ persist.ts
β”‚
β”‚ β”œβ”€β”€ services/
β”‚ β”‚ β”œβ”€β”€ api/
β”‚ β”‚ β”‚ β”œβ”€β”€ client.ts
β”‚ β”‚ β”‚ β”œβ”€β”€ refresh.ts
β”‚ β”‚ β”œβ”€β”€ storage/
β”‚ β”‚ β”‚ └── secure.ts
β”‚ β”‚
β”‚ β”œβ”€β”€ lib/
β”‚ β”‚ └── react-query.ts
β”‚
β”‚ β”œβ”€β”€ utils/
β”‚ β”‚ └── logger.ts
β”‚
β”œβ”€β”€ tailwind.config.js
β”œβ”€β”€ babel.config.js
β”œβ”€β”€ tsconfig.json

module.exports = {
content: ["./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"],
theme: { extend: {} },
plugins: [],
};

module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: ["nativewind/babel"],
};
};

{
"compilerOptions": {
"strict": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
retry: 2,
},
},
});

import axios from "axios";
import { useAuthStore } from "@/features/auth/store";
export const apiClient = axios.create({
baseURL: "https://api.example.com",
});
apiClient.interceptors.request.use((config) => {
const token = useAuthStore.getState().accessToken;
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});

import { create } from "zustand";
type AuthState = {
accessToken: string | null;
setTokens: (token: string) => void;
logout: () => void;
};
export const useAuthStore = create<AuthState>((set) => ({
accessToken: null,
setTokens: (token) => set({ accessToken: token }),
logout: () => set({ accessToken: null }),
}));

import * as SecureStore from "expo-secure-store";
export const storage = {
set: (k: string, v: string) => SecureStore.setItemAsync(k, v),
get: (k: string) => SecureStore.getItemAsync(k),
remove: (k: string) => SecureStore.deleteItemAsync(k),
};

import { apiClient } from "@/services/api/client";
export const login = async (email: string, password: string) => {
const { data } = await apiClient.post("/auth/login", {
email,
password,
});
return data;
};

import { useMutation } from "@tanstack/react-query";
import { login } from "./api";
import { useAuthStore } from "./store";
import { storage } from "@/services/storage/secure";
export const useLogin = () => {
const setToken = useAuthStore((s) => s.setTokens);
return useMutation({
mutationFn: login,
onSuccess: async (data) => {
setToken(data.accessToken);
await storage.set("token", data.accessToken);
},
});
};

import { Stack } from "expo-router";
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "@/lib/react-query";
export default function Layout() {
return (
<QueryClientProvider client={queryClient}>
<Stack />
</QueryClientProvider>
);
}

import { View, TextInput, Button } from "react-native";
import { useState } from "react";
import { useLogin } from "@/features/auth/hooks";
export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const { mutate } = useLogin();
return (
<View className="flex-1 justify-center p-4">
<TextInput
placeholder="Email"
onChangeText={setEmail}
className="border p-2 mb-2"
/>
<TextInput
placeholder="Password"
secureTextEntry
onChangeText={setPassword}
className="border p-2 mb-2"
/>
<Button title="Login" onPress={() => mutate({ email, password })} />
</View>
);
}

import { View, Text } from "react-native";
import { useAuthStore } from "@/features/auth/store";
export default function Home() {
const token = useAuthStore((s) => s.accessToken);
return (
<View className="flex-1 items-center justify-center">
<Text>Logged in βœ…</Text>
<Text>{token}</Text>
</View>
);
}

Terminal window
bun install
bunx expo start

βœ… Clean scalable architecture βœ… Auth system (JWT ready) βœ… API layer (Axios + interceptors) βœ… State management (Zustand) βœ… Server state (React Query) βœ… Styling (NativeWind) βœ… Secure storage


If you want to evolve this into a real production system, next upgrades would be:

  • πŸ”„ Refresh token system (I showed earlier)
  • πŸ“¦ Offline cache persistence
  • 🧠 Global error boundary
  • πŸ“Š Sentry logging integration
  • πŸ” Biometric auth (FaceID/Fingerprint)
  • πŸš€ CI/CD with EAS

If you want, I can convert this into: πŸ‘‰ a GitHub-ready repo (with README, scripts, env, CI/CD) πŸ‘‰ or a WhatsApp-style / fintech-grade app starter (aligned with your SaaS idea WhatsProp)