Build a Full-Stack Food Delivery App with React Native/React (2025 Guide)

sunny gsunny g
10 min read

Food delivery apps are everywhere — Swiggy, DoorDash, Uber Eats — and for good reason. They combine real-time data, smooth UI, and secure transactions into one addictive product.

But here’s the thing: you don’t just have to use them — you can build one yourself.

In this guide, we’ll walk through creating a full-stack Food Delivery App using React Native — the perfect project to level up your mobile development skills in 2025.


What You’ll Build

A cross-platform mobile app (iOS & Android) where users can:

  • Log in with Google

  • Browse restaurants with dynamic search & filtering

  • Add items to a cart and place an order

  • Navigate smoothly between screens

  • Handle backend API calls & data storage


Why This Project Rocks for Your Portfolio

  • Full-stack skills → frontend UI + backend API + database

  • Mobile-first → works on both iOS and Android

  • Scalable architecture → good habits from day one

  • Real-world relevance → employers love practical apps


Tech Stack

Frontend (React Native):

  • Expo for development speed

  • React Navigation for routing

  • Redux Toolkit or Zustand for state management

  • Axios/Fetch for API calls

Backend:

  • Node.js + Express

  • MongoDB (with Mongoose) or Firebase Firestore

  • Google OAuth API for authentication


Step 1 — Project Setup

npx create-expo-app food-delivery
cd food-delivery
npm install @react-navigation/native @react-navigation/native-stack
npm install axios react-native-paper zustand

Set up your navigation container and basic screens.


Step 2 — Google Authentication

Use Expo Auth Session or Firebase Auth for a quick, secure Google login.

import * as Google from 'expo-auth-session/providers/google';
  • Configure client IDs in Google Cloud Console

  • Store tokens securely using SecureStore


Step 3 — Restaurant List with Search & Filtering

  • Fetch restaurant data from your backend (or mock JSON for now)

  • Use a SearchBar component to filter by name/cuisine

  • Add category filter buttons (e.g., “Pizza”, “Sushi”, “Indian”)

💡 Pro tip: Implement debounced search to avoid lag when typing.


Step 4 — Cart Logic & Navigation

  • Store cart state in Zustand or Redux

  • Add “Add to Cart” buttons in restaurant menu items

  • Create a Cart Screen showing all selected items, total price, and “Checkout” button


Step 5 — Backend API

Using Node.js + Express:

  • /restaurants → list all restaurants

  • /menu/:id → get menu for a restaurant

  • /order → create a new order

If you use MongoDB:

const restaurantSchema = new mongoose.Schema({
  name: String,
  category: String,
  menu: Array
});

Step 6 — Smooth UX Touches

  • Pull-to-refresh for restaurant lists

  • Skeleton loaders while data loads

  • Animated transitions between screens


Step 7 — Deployment

  • Backend → Render/Railway/Fly.io

  • Mobile App → EAS (Expo Application Services) for iOS/Android builds


Extra Features You Can Add

  • Real-time order tracking with Socket.IO

  • Payment gateway integration (Stripe/PayPal)

  • Push notifications for order updates


Final Thoughts

Building a Food Delivery App in React Native isn’t just a coding challenge — it’s a chance to think like a product developer. You’ll learn:

  • Authentication flows

  • Data filtering & search logic

  • Cart & checkout mechanics

  • How to connect frontend and backend in a scalable way

By the end, you won’t just have an app — you’ll have a portfolio project that shows employers you can take an idea from scratch to production.


Full-stack Food Delivery web app using React (Vite) for the frontend and Node.js + Express + MongoDB for the backend.


Project structure (suggested)

food-delivery/
├─ backend/
│  ├─ package.json
│  ├─ server.js
│  ├─ .env
│  ├─ models/
│  │  ├─ Restaurant.js
│  │  └─ Order.js
│  └─ routes/
│     ├─ restaurants.js
│     └─ orders.js
└─ frontend/
   ├─ package.json
   ├─ vite.config.ts
   ├─ src/
   │  ├─ main.tsx
   │  ├─ App.tsx
   │  ├─ api.ts
   │  ├─ store/
   │  │  └─ cartStore.ts
   │  ├─ pages/
   │  │  ├─ Home.tsx
   │  │  ├─ Cart.tsx
   │  │  └─ Restaurant.tsx
   │  └─ components/
   │     ├─ SearchBar.tsx
   │     └─ RestaurantCard.tsx
   └─ .env

Backend — Node.js + Express + MongoDB

backend/package.json

{
  "name": "food-delivery-backend",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.0.0",
    "express": "^4.18.2",
    "mongoose": "^7.0.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.20"
  }
}

backend/.env (example)

PORT=5000
MONGO_URI=mongodb+srv://<user>:<pass>@cluster0.mongodb.net/foodapp?retryWrites=true&w=majority

backend/server.js

require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');

const restaurantsRoute = require('./routes/restaurants');
const ordersRoute = require('./routes/orders');

const app = express();
app.use(cors());
app.use(express.json());

app.use('/api/restaurants', restaurantsRoute);
app.use('/api/orders', ordersRoute);

const PORT = process.env.PORT || 5000;
mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(()=> {
    console.log('MongoDB connected');
    app.listen(PORT, ()=> console.log(`Server running on ${PORT}`));
  })
  .catch(err => console.error(err));

backend/models/Restaurant.js

const mongoose = require('mongoose');

const ItemSchema = new mongoose.Schema({
  name: String,
  price: Number,
  description: String,
  image: String
});

const RestaurantSchema = new mongoose.Schema({
  name: String,
  category: String,
  rating: Number,
  menu: [ItemSchema],
  location: String
});

module.exports = mongoose.model('Restaurant', RestaurantSchema);

backend/models/Order.js

const mongoose = require('mongoose');

const OrderSchema = new mongoose.Schema({
  userId: String,
  items: [{ itemId: String, name: String, price: Number, qty: Number }],
  total: Number,
  status: { type: String, default: 'pending' },
  createdAt: { type: Date, default: Date.now }
});

module.exports = mongoose.model('Order', OrderSchema);

backend/routes/restaurants.js

const express = require('express');
const router = express.Router();
const Restaurant = require('../models/Restaurant');

// GET /api/restaurants?search=&category=
router.get('/', async (req, res) => {
  const { search = '', category } = req.query;
  const filter = {
    name: { $regex: search, $options: 'i' }
  };
  if (category) filter.category = category;
  const restaurants = await Restaurant.find(filter).limit(50);
  res.json(restaurants);
});

// GET /api/restaurants/:id
router.get('/:id', async (req, res) => {
  const r = await Restaurant.findById(req.params.id);
  res.json(r);
});

module.exports = router;

backend/routes/orders.js

const express = require('express');
const router = express.Router();
const Order = require('../models/Order');

router.post('/', async (req, res) => {
  const { userId, items, total } = req.body;
  const order = new Order({ userId, items, total });
  await order.save();
  res.status(201).json(order);
});

router.get('/:userId', async (req, res) => {
  const orders = await Order.find({ userId: req.params.userId }).sort({ createdAt: -1 });
  res.json(orders);
});

module.exports = router;

Frontend — React (Vite, TypeScript) + Zustand + Axios

frontend/package.json

{
  "name": "food-delivery-frontend",
  "version": "1.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "axios": "^1.4.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.13.0",
    "zustand": "^4.2.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "vite": "^5.0.0"
  }
}

frontend/.env (example)

VITE_API_BASE=http://localhost:5000/api
VITE_FIREBASE_API_KEY=...
VITE_FIREBASE_AUTH_DOMAIN=...
VITE_FIREBASE_PROJECT_ID=...

frontend/src/api.ts

import axios from 'axios';

const API = axios.create({
  baseURL: import.meta.env.VITE_API_BASE || 'http://localhost:5000/api'
});

export const fetchRestaurants = (search = '', category = '') =>
  API.get('/restaurants', { params: { search, category } }).then(r => r.data);

export const fetchRestaurant = (id: string) =>
  API.get(`/restaurants/${id}`).then(r => r.data);

export const createOrder = (payload: any) =>
  API.post('/orders', payload).then(r => r.data);

frontend/src/store/cartStore.ts

import create from 'zustand';

type CartItem = { id: string; name: string; price: number; qty: number };

type State = {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  updateQty: (id: string, qty: number) => void;
  clear: () => void;
  total: () => number;
};

export const useCart = create<State>((set, get) => ({
  items: [],
  addItem: (item) => {
    const items = get().items.slice();
    const idx = items.findIndex(i => i.id === item.id);
    if (idx >= 0) items[idx].qty += item.qty;
    else items.push(item);
    set({ items });
  },
  removeItem: (id) => set({ items: get().items.filter(i => i.id !== id) }),
  updateQty: (id, qty) => set({
    items: get().items.map(i => i.id === id ? { ...i, qty } : i)
  }),
  clear: () => set({ items: [] }),
  total: () => get().items.reduce((s, i) => s + i.price * i.qty, 0)
}));

frontend/src/main.tsx

import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import App from './App';
import Home from './pages/Home';
import Cart from './pages/Cart';
import Restaurant from './pages/Restaurant';

createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App />} >
          <Route index element={<Home />} />
          <Route path="restaurant/:id" element={<Restaurant />} />
          <Route path="cart" element={<Cart />} />
        </Route>
      </Routes>
    </BrowserRouter>
  </React.StrictMode>
);

frontend/src/App.tsx

import React from 'react';
import { Outlet, Link } from 'react-router-dom';

export default function App(){
  return (
    <div>
      <header style={{padding:20, borderBottom:'1px solid #eee'}}>
        <Link to="/"><h2>Foodly (Demo)</h2></Link>
        <nav style={{float:'right'}}>
          <Link to="/cart">Cart</Link>
        </nav>
      </header>
      <main style={{padding:20}}>
        <Outlet />
      </main>
    </div>
  );
}

frontend/src/pages/Home.tsx

import React, { useEffect, useState } from 'react';
import { fetchRestaurants } from '../api';
import RestaurantCard from '../components/RestaurantCard';
import SearchBar from '../components/SearchBar';

export default function Home(){
  const [restaurants, setRestaurants] = useState<any[]>([]);
  const [search, setSearch] = useState('');
  const [category, setCategory] = useState('');

  useEffect(()=>{
    let mounted = true;
    fetchRestaurants(search, category).then(data => { if(mounted) setRestaurants(data); });
    return ()=> { mounted = false; }
  }, [search, category]);

  return (
    <div>
      <SearchBar value={search} onChange={setSearch} />
      <div style={{marginTop:10}}>
        <button onClick={()=>setCategory('')}>All</button>
        <button onClick={()=>setCategory('Pizza')}>Pizza</button>
        <button onClick={()=>setCategory('Indian')}>Indian</button>
      </div>
      <div style={{display:'grid', gridTemplateColumns:'repeat(3,1fr)', gap:16, marginTop:20}}>
        {restaurants.map(r => <RestaurantCard key={r._id} restaurant={r} />)}
      </div>
    </div>
  );
}

frontend/src/components/SearchBar.tsx

import React from 'react';

export default function SearchBar({ value, onChange }: { value: string; onChange: (s: string) => void }){
  return (
    <input
      value={value}
      onChange={e => onChange(e.target.value)}
      placeholder="Search restaurants or dishes..."
      style={{width:'100%', padding:10, fontSize:16}}
    />
  );
}

frontend/src/components/RestaurantCard.tsx

import React from 'react';
import { Link } from 'react-router-dom';

export default function RestaurantCard({ restaurant }: any){
  return (
    <div style={{border:'1px solid #ddd', padding:12, borderRadius:6}}>
      <h3>{restaurant.name}</h3>
      <p>{restaurant.category} • ⭐ {restaurant.rating}</p>
      <Link to={`/restaurant/${restaurant._id}`}>View menu</Link>
    </div>
  );
}

frontend/src/pages/Restaurant.tsx

import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { fetchRestaurant } from '../api';
import { useCart } from '../store/cartStore';

export default function Restaurant(){
  const { id } = useParams();
  const [rest, setRest] = useState<any>(null);
  const addItem = useCart(s => s.addItem);

  useEffect(()=>{
    if(id) fetchRestaurant(id).then(r => setRest(r));
  }, [id]);

  if(!rest) return <div>Loading...</div>;

  return (
    <div>
      <h2>{rest.name}</h2>
      <p>{rest.category} • {rest.location}</p>
      <div style={{display:'grid', gridTemplateColumns:'repeat(2,1fr)', gap:12}}>
        {rest.menu.map((m:any) => (
          <div key={m._id} style={{border:'1px solid #eee', padding:8}}>
            <h4>{m.name}</h4>
            <p>{m.description}</p>
            <div style={{display:'flex', justifyContent:'space-between', alignItems:'center'}}>
              <strong>₹{m.price}</strong>
              <button onClick={()=> addItem({ id: m._id, name: m.name, price: m.price, qty: 1 })}>Add to cart</button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

frontend/src/pages/Cart.tsx

import React from 'react';
import { useCart } from '../store/cartStore';
import { createOrder } from '../api';

export default function Cart(){
  const items = useCart(s => s.items);
  const total = useCart(s => s.total());
  const clear = useCart(s => s.clear);

  const checkout = async () => {
    // demo userId
    await createOrder({ userId: 'demo-user', items, total });
    alert('Order placed (demo)!');
    clear();
  };

  return (
    <div>
      <h2>Cart</h2>
      {items.length === 0 && <p>Cart is empty</p>}
      {items.map(i => (
        <div key={i.id} style={{display:'flex', justifyContent:'space-between'}}>
          <div>{i.name} x{i.qty}</div>
          <div>₹{i.qty * i.price}</div>
        </div>
      ))}
      <hr />
      <h3>Total: ₹{total}</h3>
      <button onClick={checkout} disabled={items.length===0}>Checkout</button>
    </div>
  );
}

Google Authentication (Firebase) — Quick guide

Using Firebase Auth saves a lot of setup:

  1. Create a Firebase project: https://console.firebase.google.com

  2. In Authentication → Sign-in method → enable Google.

  3. Get your Firebase config keys (apiKey, authDomain, projectId...). Put them in frontend/.env as VITE_FIREBASE_*.

You can integrate Firebase in React quickly (example skeleton):

// firebase.ts
import { initializeApp } from "firebase/app";
import { getAuth, GoogleAuthProvider, signInWithPopup } from "firebase/auth";

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID
};

const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const provider = new GoogleAuthProvider();

export const signInWithGoogle = async () => {
  const result = await signInWithPopup(auth, provider);
  // send result.user to backend or store in frontend state
  return result.user;
};

Add a Login button in header that calls signInWithGoogle() and saves the user in local state.


Seed data (quick JSON for restaurants)

You can insert sample restaurants into MongoDB (via script or MongoDB Atlas UI):

[
  {
    "name": "Pizza Planet",
    "category": "Pizza",
    "rating": 4.5,
    "location": "Sector 21",
    "menu": [
      { "name": "Margherita", "price": 249, "description": "Classic", "image": "" },
      { "name": "Pepperoni", "price": 349, "description": "Spicy", "image": "" }
    ]
  },
  {
    "name": "Curry Corner",
    "category": "Indian",
    "rating": 4.6,
    "location": "MG Road",
    "menu": [
      { "name": "Butter Chicken", "price": 299, "description": "Creamy", "image": "" },
      { "name": "Paneer Tikka", "price": 199, "description": "Grilled", "image": "" }
    ]
  }
]

How to run (local)

Backend

cd backend
npm install
# create .env with MONGO_URI
npm run dev

Frontend

cd frontend
npm install
# create .env with VITE_API_BASE pointing to backend, and Firebase keys if using auth
npm run dev

Open the frontend URL (Vite default http://localhost:5173) and test.


Features you can add in future developement-

  • Add pagination & server-side search for large datasets.

  • Add user profiles, addresses, and order history.

  • Integrate real payments (Stripe) and order status updates (Socket.IO).

  • Add image upload for menu items (S3 / Cloud Storage).

  • Harden security: validate server-side, rate limit, sanitize input.


Deployment tips

  • Backend: Deploy to Render / Railway / Fly.io / Heroku (MongoDB Atlas for DB).

  • Frontend: Deploy to Vercel or Netlify (both support Vite builds).

  • If you prefer a single static-hosted approach (no custom backend) you can use Firebase Cloud Firestore + Firebase Functions for a serverless variant.

0
Subscribe to my newsletter

Read articles from sunny g directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

sunny g
sunny g

I am a full-stack developer with 8+ years of experience, passionate about the JavaScript ecosystem. I have a bachelor's degree in computer science. I am most skilled and passionate about Angular and React. I am able to provide meaningful contributions to the design, installation, testing, and maintenance of any type of software system. I like to challenge myself in new roles. I have built and successfully delivered applications in multiple domains. In my free time, I like to write blogs related to software development. I have the pleasure of working on exciting projects across industries. The applications that I developed were scalable, deployable, and maintainable. I have a vision of providing cutting-edge web solutions and services to enterprises. Developed zero-to-one products.