A React Native job portal connecting overseas job seekers with employers — built with Expo Router and TypeScript.

BHCJobs is a production-connected mobile job portal targeting overseas job seekers — workers looking for opportunities abroad who need a fast, reliable way to browse listings, verify employers, and shortlist roles for later. It pulls real data from the BHCJobs REST API, handles full OTP-based authentication, and persists sessions across app restarts with AsyncStorage.
Overview
Most job portals built as portfolio pieces mock their data. BHCJobs does not. The app connects to a live backend, authenticates users through a phone-number OTP flow, and syncs shortlisted jobs back to the API on every change. The architecture mirrors what you would ship at a small company: typed API responses, a centralized Axios instance with interceptors, file-based navigation via Expo Router, and persistent sessions.
The user journey covers the full cycle: browse industries and companies, view detailed job postings, save favorites, and manage the shortlist from a dedicated tab — all without losing state on restart.
BHCJobs communicates with a live backend. Authentication uses phone number + OTP verification. Bearer tokens are stored in AsyncStorage and automatically attached to every subsequent request via an Axios interceptor — no manual header management anywhere in the UI code.
Features
| Feature | Details |
|---|---|
| Phone OTP auth | Register or log in with a phone number; verify with a time-limited OTP code |
| Forgot password | OTP-based password reset without email dependency |
| Industry browser | Browse job categories with live counts, tap through to filtered listings |
| Company profiles | Dedicated screens with company info and associated open positions |
| Job detail screen | Full posting view with description, requirements, and application CTA |
| Shortlisting | Save and unsave jobs, synced to the API; persists across sessions |
| Token session | Bearer token stored in AsyncStorage, refreshed through Axios interceptor |
Tech Stack
| Tool | Role |
|---|---|
| Expo / React Native | Cross-platform iOS and Android from a single codebase |
| Expo Router 6 | File-based navigation with typed routes |
| TypeScript | Strict typing across all API response shapes and component props |
| Axios | HTTP client with auth interceptor and request/response transformation |
| AsyncStorage | Persistent key-value store for session token and user data |
Auth Flow
Enter phone number
The registration or login screen accepts a phone number with country prefix. On submit, the API dispatches an OTP to the number.
Verify OTP
A timed OTP entry screen validates the code. Invalid or expired codes return a typed error that surfaces inline without crashing the screen.
Session established
On success, the API returns a Bearer token. It is written to AsyncStorage and injected into every subsequent Axios request automatically.
Token intercepted on every request
A single Axios instance created at app startup reads the stored token before each request. Expired token responses trigger a logout redirect rather than silent failures.
API Layer
All API calls go through a typed service layer rather than being scattered across screen files. Each endpoint returns a typed response or throws a structured error.
src/services/api.ts
import axios from 'axios'
import AsyncStorage from '@react-native-async-storage/async-storage'
export const api = axios.create({
baseURL: process.env.EXPO_PUBLIC_API_URL,
timeout: 10_000,
})
api.interceptors.request.use(async (config) => {
const token = await AsyncStorage.getItem('auth_token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})src/services/jobs.ts
import { api } from './api'
import type { Job, ShortlistResponse } from '@/types'
export const getJobById = (id: string): Promise<Job> =>
api.get(`/jobs/${id}`).then((r) => r.data)
export const shortlistJob = (jobId: string): Promise<ShortlistResponse> =>
api.post('/shortlist', { job_id: jobId }).then((r) => r.data)
export const removeShortlist = (jobId: string): Promise<void> =>
api.delete(`/shortlist/${jobId}`)Navigation Structure
Expo Router maps the app/ directory to routes directly. Protected routes check for a stored token before rendering; unauthenticated visitors are redirected to the auth stack without any manual route guard configuration.
app/
├── (auth)/
│ ├── login.tsx
│ ├── register.tsx
│ ├── verify-otp.tsx
│ └── forgot-password.tsx
├── (tabs)/
│ ├── index.tsx # Job listings
│ ├── companies.tsx
│ ├── industries.tsx
│ └── shortlist.tsx
└── jobs/
└── [id].tsx # Job detail