MediumRN_ TODO. Project — top-level layout | by Learning with Pukar | Medium
Sitemap
Learning with Pukar

Learning with Pukar

RN_ TODO

8 min readAug 15, 2025

--

Project — top-level layout

todo-app-fullstack/
├─ backend/
│ ├─ package.json
│ ├─ .env.example
│ └─ src/
│ ├─ index.js # app entry
│ ├─ config/
│ │ └─ db.js
│ ├─ models/
│ │ └─ Task.js
│ ├─ controllers/
│ │ └─ taskController.js
│ ├─ routes/
│ │ └─ taskRoutes.js
│ └─ middleware/
│ └─ errorHandler.js
│
└─ frontend/
├─ package.json
├─ app.json
├─ eas.json
└─ src/
├─ App.js
├─ config/
│ └─ constants.js
├─ services/
│ └─ api.js
├─ components/
│ ├─ TaskCard.jsx
│ └─ TaskForm.jsx
└─ screens/
├─ TaskListScreen.jsx
├─ AddTaskScreen.jsx
└─ TaskDetailScreen.jsx

Part A — Backend (organized)

Backend dependencies & init

Commands to set up:

mkdir backend && cd backend
npm init -y
npm install express mongoose dotenv cors morgan
npm install --save-dev nodemon

Add scripts in package.json (see file content below) so you can npm run dev.

File: backend/package.json

{
"name": "todo-backend",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"dev": "nodemon src/index.js",
"start": "node src/index.js"
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"mongoose": "^8.5.2",
"morgan": "^1.10.0"
},
"devDependencies": {
"nodemon": "^2.0.22"
}
}

File: backend/.env.example

MONGODB_URI=mongodb+srv://<username>:<password>@<cluster>/<dbname>?retryWrites=true&w=majority
PORT=8080

Copy to .env and replace placeholders.

File: backend/src/index.js

// src/index.js
const express = require('express');
const morgan = require('morgan');
const cors = require('cors');
const dotenv = require('dotenv');
dotenv.config();const connectDB = require('./config/db');
const taskRoutes = require('./routes/taskRoutes');
const { notFound, errorHandler } = require('./middleware/errorHandler');
const app = express();connectDB();app.use(cors());
app.use(express.json());
app.use(morgan('dev'));
app.get('/', (req, res) => res.json({ status: 'ok', message: 'To-Do API' }));app.use('/api/tasks', taskRoutes);app.use(notFound);
app.use(errorHandler);
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`));

File: backend/src/config/db.js

// src/config/db.js
const mongoose = require('mongoose');
const connectDB = async () => {
const uri = process.env.MONGODB_URI;
if (!uri) {
console.error('MONGODB_URI is not set in environment');
process.exit(1);
}
try {
await mongoose.connect(uri, {
// options not required for Mongoose 6+, but safe to include
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('✅ MongoDB connected');
} catch (err) {
console.error('❌ MongoDB connection failed:', err.message);
process.exit(1);
}
};
module.exports = connectDB;

File: backend/src/models/Task.js

// src/models/Task.js
const mongoose = require('mongoose');
const taskSchema = new mongoose.Schema(
{
title: { type: String, required: true, trim: true },
description: { type: String, trim: true, default: '' },
completed: { type: Boolean, default: false },
createdAt: { type: Date, default: Date.now },
dueDate: { type: Date } // optional
},
{ versionKey: false }
);
module.exports = mongoose.model('Task', taskSchema);

File: backend/src/controllers/taskController.js

// src/controllers/taskController.js
const Task = require('../models/Task');
// Create
exports.createTask = async (req, res, next) => {
try {
const { title, description, dueDate } = req.body;
if (!title) return res.status(400).json({ message: 'title is required' });
const task = await Task.create({ title, description, dueDate });
res.status(201).json(task);
} catch (err) {
next(err);
}
};
// Read all
exports.getTasks = async (req, res, next) => {
try {
const tasks = await Task.find().sort({ createdAt: -1 });
res.json(tasks);
} catch (err) {
next(err);
}
};
// Read one
exports.getTaskById = async (req, res, next) => {
try {
const task = await Task.findById(req.params.id);
if (!task) return res.status(404).json({ message: 'Task not found' });
res.json(task);
} catch (err) {
next(err);
}
};
// Update
exports.updateTask = async (req, res, next) => {
try {
const updates = req.body;
const task = await Task.findByIdAndUpdate(req.params.id, updates, {
new: true,
runValidators: true
});
if (!task) return res.status(404).json({ message: 'Task not found' });
res.json(task);
} catch (err) {
next(err);
}
};
// Delete
exports.deleteTask = async (req, res, next) => {
try {
const task = await Task.findByIdAndDelete(req.params.id);
if (!task) return res.status(404).json({ message: 'Task not found' });
res.json({ message: 'Task deleted' });
} catch (err) {
next(err);
}
};

File: backend/src/routes/taskRoutes.js

// src/routes/taskRoutes.js
const express = require('express');
const router = express.Router();
const {
createTask,
getTasks,
getTaskById,
updateTask,
deleteTask
} = require('../controllers/taskController');
// Endpoints per rubric
router.post('/add', createTask); // POST /api/tasks/add
router.get('/', getTasks); // GET /api/tasks
router.get('/:id', getTaskById); // GET /api/tasks/:id
router.put('/update/:id', updateTask); // PUT /api/tasks/update/:id
router.delete('/delete/:id', deleteTask); // DELETE /api/tasks/delete/:id
module.exports = router;

File: backend/src/middleware/errorHandler.js

// src/middleware/errorHandler.js
exports.notFound = (req, res, next) => {
res.status(404);
next(new Error(`Not Found - ${req.originalUrl}`));
};
exports.errorHandler = (err, req, res, next) => {
const statusCode = res.statusCode && res.statusCode !== 200 ? res.statusCode : 500;
res.status(statusCode).json({
message: err.message || 'Server Error'
});
};

How to run backend locally

  1. Copy .env.example → .env and set MONGODB_URI.
  2. Install & start:
cd backend
npm install
npm run dev

API will run on http://localhost:8080 (or PORT you set). Example endpoints:

  • POST /api/tasks/add
  • GET /api/tasks
  • GET /api/tasks/:id
  • PUT /api/tasks/update/:id
  • DELETE /api/tasks/delete/:id

Deploy tip: Render, Heroku, or Vercel (Serverless Node) are fine. If deploying to Render, add MONGODB_URI env var and start command node src/index.js.

Part B — Frontend (React Native + Expo)

Frontend dependencies & init

Commands:

# Create app
npx create-expo-app frontend
cd frontend
# Install required libs
npm install axios @react-navigation/native @react-navigation/native-stack
npx expo install react-native-screens react-native-safe-area-context
# (Optional for EAS build)
npm install -D eas-cli

If you prefer bare React Native CLI instead of Expo, update instructions accordingly. I use Expo for easy builds and EAS APK creation.

File: frontend/package.json

(Generated by create-expo-app — include these dependencies)

{
"name": "todo-frontend",
"version": "1.0.0",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web"
},
"dependencies": {
"axios": "^1.7.7",
"expo": "~51.0.0",
"react": "18.2.0",
"react-native": "0.74.0",
"@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.9.27"
}
}

File: frontend/app.json (minimal)

{
"expo": {
"name": "ToDo App",
"slug": "todo-app",
"version": "1.0.0",
"platforms": ["android","ios","web"],
"android": { "package": "com.yourname.todoapp" }
}
}

File: frontend/eas.json (for APK builds)

{
"cli": { "version": ">= 9.0.0" },
"build": {
"preview": { "android": { "buildType": "apk" } },
"production": { "android": { "gradleCommand": ":app:bundleRelease" } }
}
}

File: frontend/src/config/constants.js

// src/config/constants.js
// Set this to your deployed backend base URL (no trailing slash), e.g. https://my-backend.onrender.com
export const API_BASE_URL = 'http://YOUR_BACKEND_HOST:8080';

When testing on an Android emulator with a local server, use 10.0.2.2 in place of localhost.

File: frontend/src/services/api.js

// src/services/api.js
import axios from 'axios';
import { API_BASE_URL } from '../config/constants';
const base = `${API_BASE_URL}/api/tasks`;const api = axios.create({
baseURL: base,
timeout: 10000
});
export const fetchTasks = () => api.get('/');
export const fetchTask = (id) => api.get(`/${id}`);
export const createTask = (payload) => api.post('/add', payload);
export const updateTask = (id, payload) => api.put(`/update/${id}`, payload);
export const removeTask = (id) => api.delete(`/delete/${id}`);
export default api;

File: frontend/src/components/TaskCard.jsx

// src/components/TaskCard.jsx
import React from 'react';
import { Pressable, View, Text, StyleSheet } from 'react-native';
export default function TaskCard({ task, onPress }) {
return (
<Pressable onPress={onPress} style={({ pressed }) => [styles.card, pressed && { opacity: 0.7 }]}>
<View>
<Text style={styles.title}>{task.title}</Text>
{task.description ? <Text style={styles.desc} numberOfLines={2}>{task.description}</Text> : null}
<Text style={styles.meta}>{task.completed ? 'Completed' : 'Pending'} • {new Date(task.createdAt).toLocaleString()}</Text>
</View>
</Pressable>
);
}
const styles = StyleSheet.create({
card: { backgroundColor: '#fff', padding: 12, marginVertical: 6, borderRadius: 8, elevation: 2 },
title: { fontSize: 16, fontWeight: '700' },
desc: { marginTop: 6, color: '#444' },
meta: { marginTop: 8, fontSize: 12, color: '#777' }
});

File: frontend/src/components/TaskForm.jsx

// src/components/TaskForm.jsx
import React, { useState, useEffect } from 'react';
import { View, TextInput, Button, StyleSheet } from 'react-native';
export default function TaskForm({ initial = { title: '', description: '', completed: false }, onSubmit, submitLabel = 'Save' }) {
const [title, setTitle] = useState(initial.title);
const [description, setDescription] = useState(initial.description);
const [completed, setCompleted] = useState(initial.completed);
useEffect(() => {
setTitle(initial.title ?? '');
setDescription(initial.description ?? '');
setCompleted(initial.completed ?? false);
}, [initial]);
return (
<View style={styles.container}>
<TextInput placeholder="Title" style={styles.input} value={title} onChangeText={setTitle} />
<TextInput placeholder="Description (optional)" style={[styles.input, styles.textArea]} value={description} onChangeText={setDescription} multiline numberOfLines={4} />
<Button title={submitLabel} onPress={() => onSubmit({ title, description, completed })} />
</View>
);
}
const styles = StyleSheet.create({
container: { gap: 12 },
input: { backgroundColor: '#fff', padding: 10, borderRadius: 8, borderColor: '#ddd', borderWidth: 1 },
textArea: { minHeight: 100, textAlignVertical: 'top' }
});

File: frontend/src/screens/TaskListScreen.jsx

// src/screens/TaskListScreen.jsx
import React, { useEffect, useState, useCallback } from 'react';
import { View, FlatList, Button, RefreshControl, ActivityIndicator, Alert } from 'react-native';
import TaskCard from '../components/TaskCard';
import { fetchTasks, removeTask } from '../services/api';
export default function TaskListScreen({ navigation }) {
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const load = useCallback(async () => {
try {
setLoading(true);
const { data } = await fetchTasks();
setTasks(data);
} catch (err) {
Alert.alert('Error', 'Unable to load tasks');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
const unsub = navigation.addListener('focus', load);
return unsub;
}, [navigation, load]);
const onRefresh = useCallback(async () => {
setRefreshing(true);
await load();
setRefreshing(false);
}, [load]);
const handleDelete = async (id) => {
try {
await removeTask(id);
await load();
} catch {
Alert.alert('Error', 'Unable to delete');
}
};
return (
<View style={{ flex: 1, padding: 16, backgroundColor: '#f4f4f6' }}>
<Button title="Add Task" onPress={() => navigation.navigate('AddTask')} />
{loading ? (
<ActivityIndicator style={{ marginTop: 20 }} />
) : (
<FlatList
style={{ marginTop: 12 }}
data={tasks}
keyExtractor={(item) => item._id}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
renderItem={({ item }) => (
<TaskCard
task={item}
onPress={() => navigation.navigate('TaskDetail', { id: item._id })}
/>
)}
/>
)}
</View>
);
}

File: frontend/src/screens/AddTaskScreen.jsx

// src/screens/AddTaskScreen.jsx
import React from 'react';
import { View, Alert } from 'react-native';
import TaskForm from '../components/TaskForm';
import { createTask } from '../services/api';
export default function AddTaskScreen({ navigation }) {
const onSubmit = async (payload) => {
try {
if (!payload.title) return Alert.alert('Validation', 'Title is required');
await createTask(payload);
navigation.goBack();
} catch (err) {
Alert.alert('Error', 'Could not create task');
}
};
return (
<View style={{ flex: 1, padding: 16 }}>
<TaskForm onSubmit={onSubmit} submitLabel="Create Task" />
</View>
);
}

File: frontend/src/screens/TaskDetailScreen.jsx

// src/screens/TaskDetailScreen.jsx
import React, { useEffect, useState } from 'react';
import { View, Button, Alert, ActivityIndicator } from 'react-native';
import TaskForm from '../components/TaskForm';
import { fetchTask, updateTask, removeTask } from '../services/api';
export default function TaskDetailScreen({ route, navigation }) {
const { id } = route.params;
const [task, setTask] = useState(null);
const [loading, setLoading] = useState(true);
const loadTask = async () => {
try {
const { data } = await fetchTask(id);
setTask(data);
} catch {
Alert.alert('Error', 'Failed to load task');
navigation.goBack();
} finally {
setLoading(false);
}
};
useEffect(() => {
loadTask();
}, []);
const onUpdate = async (payload) => {
try {
await updateTask(id, payload);
navigation.goBack();
} catch {
Alert.alert('Error', 'Failed to update task');
}
};
const onDelete = async () => {
try {
await removeTask(id);
navigation.goBack();
} catch {
Alert.alert('Error', 'Failed to delete');
}
};
if (loading) return <ActivityIndicator style={{ marginTop: 24 }} />; return (
<View style={{ flex: 1, padding: 16 }}>
<TaskForm initial={task} onSubmit={onUpdate} submitLabel="Save Changes" />
<View style={{ height: 12 }} />
<Button title="Delete Task" color="#cc0000" onPress={onDelete} />
</View>
);
}

File: frontend/src/App.js

// src/App.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import TaskListScreen from './screens/TaskListScreen';
import AddTaskScreen from './screens/AddTaskScreen';
import TaskDetailScreen from './screens/TaskDetailScreen';
const Stack = createNativeStackNavigator();export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Tasks" component={TaskListScreen} />
<Stack.Screen name="AddTask" component={AddTaskScreen} options={{ title: 'Add Task' }} />
<Stack.Screen name="TaskDetail" component={TaskDetailScreen} options={{ title: 'Task Detail' }} />
</Stack.Navigator>
</NavigationContainer>
);
}

How to run the frontend locally

  1. Set API_BASE_URL in src/config/constants.js to your backend URL (http://<IP-or-domain>:PORT).
  2. Install & start:
cd frontend
npm install
npm run start

Open on device with Expo Go or press a to run on Android emulator.

Get Learning with Pukar’s stories in your inbox

Join Medium for free to get updates from this writer.

Note for Android emulator & local backend: Use 10.0.2.2 in API_BASE_URL instead of localhost.

Extra: Deployment & APK (packaging)

  • Backend: push backend/ to GitHub and deploy to Render / Heroku / Railway. Make sure MONGODB_URI is set in env.
  • Frontend APK:
  1. Install EAS CLI: npm install -g eas-cli.
  2. Log in: eas login.
  3. Configure eas.json (provided).
  4. Build: eas build -p android --profile preview (this produces an .apk you can download).
  5. Upload .apk to submission link or shareable URL.

Quick checklist (so you don’t miss anything)

  • Create MongoDB Atlas cluster, add user, copy connection string.
  • Fill backend/.env with MONGODB_URI and PORT.
  • cd backend → npm install → npm run dev (verify GET /api/tasks).
  • cd frontend → update API_BASE_URL → npm install → npm run start.
  • Test flows: Create, List, Read, Update, Delete tasks.
  • Deploy backend (Render/Heroku) and update front API_BASE_URL to deployed URL.
  • Build APK with EAS for final submission.

--

--