RN_ TODO
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.jsxPart 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 nodemonAdd 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=8080Copy to
.envand 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/:idmodule.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
- Copy
.env.example→.envand setMONGODB_URI. - Install & start:
cd backend
npm install
npm run devAPI will run on http://localhost:8080 (or PORT you set). Example endpoints:
POST /api/tasks/addGET /api/tasksGET /api/tasks/:idPUT /api/tasks/update/:idDELETE /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.2in place oflocalhost.
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
- Set
API_BASE_URLinsrc/config/constants.jsto your backend URL (http://<IP-or-domain>:PORT). - Install & start:
cd frontend
npm install
npm run startOpen 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 sureMONGODB_URIis set in env. - Frontend APK:
- Install EAS CLI:
npm install -g eas-cli. - Log in:
eas login. - Configure
eas.json(provided). - Build:
eas build -p android --profile preview(this produces an.apkyou can download). - Upload
.apkto submission link or shareable URL.
Quick checklist (so you don’t miss anything)
- Create MongoDB Atlas cluster, add user, copy connection string.
- Fill
backend/.envwithMONGODB_URIandPORT. cd backend→npm install→npm run dev(verifyGET /api/tasks).cd frontend→ updateAPI_BASE_URL→npm install→npm run start.- Test flows: Create, List, Read, Update, Delete tasks.
- Deploy backend (Render/Heroku) and update front
API_BASE_URLto deployed URL. - Build APK with EAS for final submission.





