Introduction
Project Requirements
Login requirements.- Users should be able to log in using user name, password or social media like google.
- Users should be able to log out, ending their session.
- Implement session management to keep users logged in across different pages.
- Users should be able to create new ToDo items. Each ToDo item should have a title, description, and due date.
- Users should be able to edit the title, description, and due date.
- Users should be able to delete ToDo items.Confirm deletion action to prevent accidental deletion.
Tech Stack
- Frontend - React, Redux, Axios, Tailwind CSS
- Backend - Node.js, Express, Prisma , PostgresSql, Passport.js, BCrypt, jsonwebtoken (JWT)
- Deployment - Some of the options are Vercel / Netlify or Heroku / DigitalOcean / AWS. We will use vercel
- Performance Optimization - Google Lighthouse
- Monitoring - Some of the options are Sentry / LogRocket/ DataDog. We will use sentry
App Models
There are 2 models (entities) in our App.User Model1// Prisma schema example
2model User {
3 id Int @id @default(autoincrement())
4 username String @unique
5 email String @unique
6 passwordHash String
7 todos Todo[]
8 createdAt DateTime @default(now())
9 updatedAt DateTime @updatedAt
10}
1// Prisma schema example
2model Todo {
3 id Int @id @default(autoincrement())
4 title String
5 completed Boolean @default(false)
6 user User @relation(fields: [userId], references: [id])
7 userId Int
8 createdAt DateTime @default(now())
9 updatedAt DateTime @updatedAt
10}
Setting Up Project
We will create seperate projects for front end and back end.
Install VSCode at VS Code
Install NodeJS at NodeJS
- Create react app
npx create-react-app my-app
- Go to project directory.
cd my-app
- Start React app!
npm start
- Now edit App.js file and you should be able to see changes in app!
npm i express
Environments and Variable Management
process.env.NODE_ENV is an environment variable used in Node.js and various JavaScript environments to indicate the current execution environment of the application. It is commonly used to differentiate between various stages of the development lifecycle, such as development, testing, and production.
Common values of NODE_ENV are- development
- production
- test
NODE_ENV=test node app.js
Then you can use below code to read values from specific environment file (.env.development, .env.test etc). Please note that you will need to install dotenv package!
1require('dotenv').config({ path: `.env.${process.env.NODE_ENV}` });
2let apiUrl = process.env.API_URL
Backend
Express
1//file - src/index.js
2
3// Import the Express module
4const express = require('express');
5
6// Create an Express application
7const app = express();
8
9// Define a port to listen on
10const PORT = process.env.PORT || 8000;
11
12// Define a route for the root URL
13app.get('/', (req, res) => {
14 res.send('Hello, World!');
15});
16
17// Start the server
18app.listen(PORT, () => {
19 console.log(`Server is running on http://localhost:${PORT}`);
20});
PostgressSql
Prisma
1npm install prisma @prisma/client
2npx prisma init
DATABASE_URL="postgresql://username:password@localhost:5432/mydatabase?schema=public"
1generator client {
2 provider = "prisma-client-js"
3}
4
5datasource db {
6 provider = "postgresql"
7 url = env("DATABASE_URL")
8}
9
10
11 model User {
12 id Int @id @default(autoincrement())
13 email String @unique
14 password String
15 username String? // Optional field
16 createdAt DateTime @default(now())
17 updatedAt DateTime @updatedAt
18 todos ToDo[] // Relationship to ToDo model
19 }
20
21 model ToDo {
22 id Int @id @default(autoincrement())
23 name String
24 status String @default("new")
25 createdAt DateTime @default(now())
26 updatedAt DateTime @updatedAt
27 userId Int // Foreign key field
28 user User @relation(fields: [userId], references: [id])
29 }
npx prisma migrate dev --name init
npx prisma generate
1//src/server/prisma.ts
2
3let prisma;
4
5if (process.env.NODE_ENV === 'production') {
6 prisma = new PrismaClient();
7} else {
8 // In development, use a singleton pattern to prevent multiple instances
9 if (!global.prisma) {
10 global.prisma = new PrismaClient();
11 }
12 prisma = global.prisma;
13}
14
15module.exports = prisma;
const prisma = require('./prisma');
APIs for Models
Now we will create APIs for User and ToDo models!
Create User Controller1const prisma = require('./prismaClient');
2
3
4exports.getAllUsers = async (req, res) => {
5 try {
6 const users = await prisma.user.findMany();
7 res.json(users);
8 } catch (error) {
9 res.status(500).json({ error: 'Error fetching users' });
10 }
11};
12
13exports.createUser = async (req, res) => {
14 try {
15 const { name, email } = req.body;
16 const user = await prisma.user.create({
17 data: { name, email },
18 });
19 res.status(201).json(user);
20 } catch (error) {
21 res.status(500).json({ error: 'Error creating user' });
22 }
23};
24
25exports.updateUser = async (req, res) => {
26 try {
27 const { id } = req.params;
28 const { name, email } = req.body;
29 const user = await prisma.user.update({
30 where: { id: parseInt(id) },
31 data: { name, email },
32 });
33 res.json(user);
34 } catch (error) {
35 res.status(500).json({ error: 'Error updating user' });
36 }
37};
38
39exports.deleteUser = async (req, res) => {
40 try {
41 const { id } = req.params;
42 await prisma.user.delete({
43 where: { id: parseInt(id) },
44 });
45 res.status(204).send();
46 } catch (error) {
47 res.status(500).json({ error: 'Error deleting user' });
48 }
49};
1const prisma = require('./prismaClient');
2
3exports.getAllTodos = async (req, res) => {
4 try {
5 const todos = await prisma.todo.findMany();
6 res.json(todos);
7 } catch (error) {
8 res.status(500).json({ error: 'Error fetching todos' });
9 }
10};
11
12exports.createTodo = async (req, res) => {
13 try {
14 const { title, userId } = req.body;
15 const todo = await prisma.todo.create({
16 data: { title, userId },
17 });
18 res.status(201).json(todo);
19 } catch (error) {
20 res.status(500).json({ error: 'Error creating todo' });
21 }
22};
23
24exports.updateTodo = async (req, res) => {
25 try {
26 const { id } = req.params;
27 const { title, completed } = req.body;
28 const todo = await prisma.todo.update({
29 where: { id: parseInt(id) },
30 data: { title, completed },
31 });
32 res.json(todo);
33 } catch (error) {
34 res.status(500).json({ error: 'Error updating todo' });
35 }
36};
37
38exports.deleteTodo = async (req, res) => {
39 try {
40 const { id } = req.params;
41 await prisma.todo.delete({
42 where: { id: parseInt(id) },
43 });
44 res.status(204).send();
45 } catch (error) {
46 res.status(500).json({ error: 'Error deleting todo' });
47 }
48};
1// file - /routes/userRoutes
2
3const express = require('express');
4const router = express.Router();
5const userController = require('../controllers/userController');
6
7router.get('/', userController.getAllUsers);
8router.post('/', userController.createUser);
9router.put('/:id', userController.updateUser);
10router.delete('/:id', userController.deleteUser);
11
12module.exports = router;
1// file - /routes/todoRoutes
2 const express = require('express');
3const router = express.Router();
4const todoController = require('../controllers/todoController');
5
6router.get('/', todoController.getAllTodos);
7router.post('/', todoController.createTodo);
8router.put('/:id', todoController.updateTodo);
9router.delete('/:id', todoController.deleteTodo);
10
11module.exports = router;
1const express = require('express');
2const bodyParser = require('body-parser');
3const userRoutes = require('./routes/userRoutes');
4const todoRoutes = require('./routes/todoRoutes');
5
6const app = express();
7const port = process.env.PORT || 3000;
8
9app.use(bodyParser.json());
10
11app.use('/users', userRoutes);
12app.use('/todos', todoRoutes);
13
14app.listen(port, () => {
15 console.log(`Server running on port ${port}`);
16});
- Get User: GET http://localhost:3000/users
- Update User: PUT http://localhost:3000/users/:id
- Delete User: DELETE http://localhost:3000/users/:id
- Get ToDo: GET http://localhost:3000/todos
- Update ToDo: PUT http://localhost:3000/todos/:id
- Delete User: DELETE http://localhost:3000/todos/:id
Authentication and Session
Install Dependenciesnpm install passport passport-local express-session bcryptjs
1const LocalStrategy = require('passport-local').Strategy;
2const bcrypt = require('bcryptjs');
3const prisma = require('./prismaClient');
4
5
6module.exports = function(passport) {
7 passport.use(
8 new LocalStrategy(
9 async (username, password, done) => {
10 try {
11 const user = await prisma.user.findUnique({ where: { email: username } });
12 if (!user) {
13 return done(null, false, { message: 'No user with that email' });
14 }
15
16 const isMatch = await bcrypt.compare(password, user.password);
17 if (isMatch) {
18 return done(null, user);
19 } else {
20 return done(null, false, { message: 'Password incorrect' });
21 }
22 } catch (err) {
23 return done(err);
24 }
25 }
26 )
27 );
28
29 passport.serializeUser((user, done) => {
30 done(null, user.id);
31 });
32
33 passport.deserializeUser(async (id, done) => {
34 try {
35 const user = await prisma.user.findUnique({ where: { id } });
36 done(null, user);
37 } catch (err) {
38 done(err);
39 }
40 });
41};
1const express = require('express');
2const session = require('express-session');
3const passport = require('passport');
4const bodyParser = require('body-parser');
5const userRoutes = require('./routes/userRoutes');
6const passportConfig = require('./passport');
7const bcrypt = require('bcryptjs');
8
9const app = express();
10const port = process.env.PORT || 3000;
11
12// Middleware setup
13app.use(bodyParser.json());
14app.use(bodyParser.urlencoded({ extended: false }));
15
16app.use(session({
17 secret: 'secret',
18 resave: false,
19 saveUninitialized: true,
20}));
21
22passportConfig(passport);
23app.use(passport.initialize());
24app.use(passport.session());
25
26// Routes
27app.use('/users', userRoutes);
28
29app.listen(port, () => {
30 console.log(`Server running on port ${port}`);
31});
1const express = require('express');
2const router = express.Router();
3const passport = require('passport');
4const bcrypt = require('bcryptjs');
5const prisma = require('./prismaClient');
6
7
8// Register route
9router.post('/register', async (req, res) => {
10 try {
11 const { name, email, password } = req.body;
12 const hashedPassword = await bcrypt.hash(password, 10);
13 const user = await prisma.user.create({
14 data: {
15 name,
16 email,
17 password: hashedPassword,
18 },
19 });
20 res.status(201).json(user);
21 } catch (error) {
22 res.status(500).json({ error: 'Error registering user' });
23 }
24});
25
26// Login route
27router.post('/login', passport.authenticate('local'), (req, res) => {
28 res.json(req.user);
29});
30
31// Logout route
32router.post('/logout', (req, res) => {
33 req.logout();
34 res.status(200).send('Logged out');
35});
36
37// Check if authenticated
38router.get('/current', (req, res) => {
39 if (req.isAuthenticated()) {
40 res.json(req.user);
41 } else {
42 res.status(401).json({ message: 'Not authenticated' });
43 }
44});
45
46module.exports = router;
Passport authentication flow
- The user submits a login form with their username (email) and password. This form submission is typically handled by a POST request to a specific route, such as /login.
- Passport's LocalStrategy Kicks In - Passport uses the configured LocalStrategy to verify the username and password.
- Session Management - Once authentication is successful, Passport serializes the user information (typically the user ID) into the session using req.logIn. Express session middleware stores the session ID on the client-side within a cookie, and the serialized user ID is stored on the server-side in a session store. On subsequent requests, Passport uses the session ID to deserialize the user by fetching the user details from the database and attaching it to req.user.
- You can protect certain routes by ensuring that only authenticated users can access them. Passport provides the req.isAuthenticated() method to check if a user is authenticated.
1router.get('/profile', (req, res) => { 2 if (req.isAuthenticated()) { 3 res.json(req.user); // User is authenticated, return their profile 4 } else { 5 res.status(401).json({ message: 'Unauthorized' }); // User is not authenticated 6 } 7});
- To log out, you can call req.logout(), which removes the user session.
- By default, express-session stores session data in memory on the server, but this is not suitable for production. In production, you typically use a session store like Redis, MongoDB, or a relational database to persist session data across server restarts or in distributed environments.
Google Provider with Passport
Install packages.npm install passport passport-google-oauth20
Create a Google OAuth 2.0 Client and set the authorized redirect URI to your application's callback URL (e.g., http://localhost:3000/auth/google/callback).
1//file - src/passport.js
2
3
4const GoogleStrategy = require('passport-google-oauth20').Strategy;
5const prisma = require('./prismaClient');
6
7
8module.exports = function (passport) {
9 passport.use(
10 new GoogleStrategy(
11 {
12 clientID: process.env.GOOGLE_CLIENT_ID,
13 clientSecret: process.env.GOOGLE_CLIENT_SECRET,
14 callbackURL: "/auth/google/callback",
15 },
16 async (accessToken, refreshToken, profile, done) => {
17 try {
18 // Find or create user in your database
19 let user = await prisma.user.findUnique({
20 where: { googleId: profile.id },
21 });
22
23 if (!user) {
24 user = await prisma.user.create({
25 data: {
26 googleId: profile.id,
27 name: profile.displayName,
28 email: profile.emails[0].value,
29 // Store additional info as needed
30 },
31 });
32 }
33
34 return done(null, user);
35 } catch (err) {
36 return done(err);
37 }
38 }
39 )
40 );
41
42 passport.serializeUser((user, done) => {
43 done(null, user.id);
44 });
45
46 passport.deserializeUser(async (id, done) => {
47 try {
48 const user = await prisma.user.findUnique({ where: { id } });
49 done(null, user);
50 } catch (err) {
51 done(err);
52 }
53 });
54};
1const express = require('express');
2const session = require('express-session');
3const passport = require('passport');
4const passportConfig = require('./passport');
5require('dotenv').config();
6
7const app = express();
8const port = process.env.PORT || 3000;
9
10app.use(express.json());
11app.use(express.urlencoded({ extended: false }));
12
13app.use(
14 session({
15 secret: 'secret',
16 resave: false,
17 saveUninitialized: true,
18 })
19);
20
21passportConfig(passport);
22app.use(passport.initialize());
23app.use(passport.session());
24
25// Routes
26app.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
27
28app.get(
29 '/auth/google/callback',
30 passport.authenticate('google', { failureRedirect: '/' }),
31 (req, res) => {
32 // Successful authentication, redirect home.
33 res.redirect('/dashboard');
34 }
35);
36
37app.get('/dashboard', (req, res) => {
38 if (req.isAuthenticated()) {
39 res.send(`Hello, ${req.user.name}`);
40 } else {
41 res.redirect('/');
42 }
43});
44
45app.get('/logout', (req, res) => {
46 req.logout(err => {
47 if (err) {
48 return next(err);
49 }
50 res.redirect('/');
51 });
52});
53
54app.listen(port, () => {
55 console.log(`Server running on port ${port}`);
56});
1GOOGLE_CLIENT_ID=your-google-client-id
2GOOGLE_CLIENT_SECRET=your-google-client-secret
1model User {
2 id Int @id @default(autoincrement())
3 googleId String? @unique
4 name String
5 email String @unique
6}
Store Session in Redis
In production, you must use database (e.g. redis or mongodb) to store session data.
Install Dependencies.npm install connect-redis ioredis
You need a Redis server running. You can either install Redis locally on your machine or Use a Redis service like Redis Cloud or Amazon ElastiCache.
Configure Express to store session data in Redis!1const express = require('express');
2const session = require('express-session');
3const RedisStore = require('connect-redis')(session);
4const Redis = require('ioredis');
5
6const app = express();
7
8// Set up Redis client
9const redisClient = new Redis({
10 host: 'localhost', // Redis server host
11 port: 6379, // Redis server port
12 password: '', // Password if Redis is protected (optional)
13});
14
15// Set up session middleware with Redis store
16app.use(
17 session({
18 store: new RedisStore({ client: redisClient }),
19 secret: 'your-secret-key', // Replace with your secret key
20 resave: false, // Avoid resaving session if not modified
21 saveUninitialized: false, // Avoid saving uninitialized sessions
22 cookie: {
23 secure: false, // Set to true if using HTTPS
24 httpOnly: true, // Prevents client-side JS from accessing the cookie
25 maxAge: 24 * 60 * 60 * 1000, // Session max age in milliseconds (1 day)
26 },
27 })
28);
29
30app.get('/', (req, res) => {
31 if (req.session.views) {
32 req.session.views++;
33 res.send(`Number of views: ${req.session.views}`);
34 } else {
35 req.session.views = 1;
36 res.send('Welcome to the site!');
37 }
38});
39
40app.listen(3000, () => {
41 console.log('Server is running on port 3000');
42});
1// Authentication status endpoint
2app.get('/auth/status', (req, res) => {
3 if (req.isAuthenticated()) {
4 res.json({
5 isAuthenticated: true,
6 user: {
7 id: req.user.id,
8 username: req.user.username,
9 },
10 });
11 } else {
12 res.json({ isAuthenticated: false });
13 }
14});
Frontend
Setting Up React Project
- Create react app
npx create-react-app my-app
- Go to project directory.
cd my-app
- Start React app!
npm start
- Now edit App.js file and you should be able to see changes in app!
Identification of Components and State
When building a Todo app, identifying components and their states is crucial for managing the application's UI and functionality. Here's a breakdown of how you might structure the components and state for a typical Todo app:
Here is the list of components- Components App: The root component that includes the overall layout and manages global state if needed.
- TodoList: Displays a list of Todo items.
- TodoItem: Represents an individual Todo item, with options to mark it as completed or delete it. This corresponds to ToDo Model.
- TodoForm: A form for adding new Todo items.
- Filter: Allows users to filter the list of Todo items (e.g., all, completed, active).
- Global State (in App component or context): todos: An array of all Todo items, filter: The current filter being applied (e.g., "all", "completed", "active").
- Todo Item State: Each Todo item can have its own state, which includes: id: A unique identifier for the Todo, text: The content of the Todo and completed: A boolean indicating whether the Todo is completed or not.
App Component
1import React, { useState, useEffect } from 'react';
2import axios from 'axios';
3import TodoList from './TodoList';
4import TodoForm from './TodoForm';
5import Filter from './Filter';
6import Auth from './Auth';
7
8function App() {
9 const [todos, setTodos] = useState([]);
10 const [filter, setFilter] = useState('all');
11 const [editingTodo, setEditingTodo] = useState(null);
12 const [isAuthenticated, setIsAuthenticated] = useState(false);
13 const [user, setUser] = useState(null);
14
15 useEffect(() => {
16
17 const handleLogin = (userData) => {
18 setIsAuthenticated(true);
19 setUser(userData);
20 };
21
22 const handleLogout = () => {
23 setIsAuthenticated(false);
24 setUser(null);
25 };
26
27 const fetchTodos = async () => {
28 try {
29 const response = await axios.get('/api/todos');
30 setTodos(response.data);
31 } catch (error) {
32 console.error('Error fetching todos', error);
33 }
34 };
35
36 fetchTodos();
37 }, []);
38
39 const addTodo = async (text) => {
40 try {
41 const response = await axios.post('/api/todos', { text });
42 setTodos([...todos, response.data]);
43 } catch (error) {
44 console.error('Error adding todo', error);
45 }
46 };
47
48 const editTodo = async (updatedTodo) => {
49 try {
50 const response = await axios.put(`/api/todos/${updatedTodo.id}`, updatedTodo);
51 setTodos(todos.map(todo =>
52 todo.id === updatedTodo.id ? response.data : todo
53 ));
54 setEditingTodo(null);
55 } catch (error) {
56 console.error('Error updating todo', error);
57 }
58 };
59
60 const toggleTodo = async (id) => {
61 try {
62 const todo = todos.find(todo => todo.id === id);
63 const updatedTodo = { ...todo, completed: !todo.completed };
64 const response = await axios.put(`/api/todos/${id}`, updatedTodo);
65 setTodos(todos.map(todo =>
66 todo.id === id ? response.data : todo
67 ));
68 } catch (error) {
69 console.error('Error updating todo', error);
70 }
71 };
72
73 const deleteTodo = async (id) => {
74 try {
75 await axios.delete(`/api/todos/${id}`);
76 setTodos(todos.filter(todo => todo.id !== id));
77 } catch (error) {
78 console.error('Error deleting todo', error);
79 }
80 };
81
82 const filteredTodos = todos.filter(todo => {
83 if (filter === 'completed') return todo.completed;
84 if (filter === 'active') return !todo.completed;
85 return true;
86 });
87
88 return (
89 <div>
90 <h1>Todo App</h1>
91 <Auth onLogin={handleLogin} onLogout={handleLogout} isAuthenticated={isAuthenticated} user={user} />
92 {isAuthenticated && (
93 <>
94 <TodoForm addTodo={addTodo} editTodo={editTodo} currentTodo={editingTodo} />
95 <Filter setFilter={setFilter} />
96 <TodoList
97 todos={filteredTodos}
98 toggleTodo={toggleTodo}
99 deleteTodo={deleteTodo}
100 setEditingTodo={setEditingTodo}
101 />
102 </>
103 )}
104 </div>
105 );
106}
107
108export default App;
To Do Form Component
1import React, { useState, useEffect } from 'react';
2
3function TodoForm({ addTodo, editTodo, currentTodo }) {
4 const [input, setInput] = useState('');
5
6 useEffect(() => {
7 if (currentTodo) {
8 setInput(currentTodo.text);
9 } else {
10 setInput('');
11 }
12 }, [currentTodo]);
13
14 const handleSubmit = async (e) => {
15 e.preventDefault();
16 if (input.trim()) {
17 if (currentTodo) {
18 await editTodo({
19 ...currentTodo,
20 text: input
21 });
22 } else {
23 await addTodo(input);
24 }
25 setInput('');
26 }
27 };
28
29 return (
30 <form onSubmit={handleSubmit}>
31 <input
32 type="text"
33 value={input}
34 onChange={(e) => setInput(e.target.value)}
35 placeholder={currentTodo ? "Edit todo" : "Add a new todo"}
36 />
37 <button type="submit">{currentTodo ? "Update Todo" : "Add Todo"}</button>
38 </form>
39 );
40}
41
42export default TodoForm;
To Do List Component
1import React from 'react';
2import TodoItem from './TodoItem';
3
4function TodoList({ todos, toggleTodo, deleteTodo, setEditingTodo }) {
5 return (
6 <ul>
7 {todos.map(todo => (
8 <TodoItem
9 key={todo.id}
10 todo={todo}
11 toggleTodo={toggleTodo}
12 deleteTodo={deleteTodo}
13 setEditingTodo={setEditingTodo}
14 />
15 ))}
16 </ul>
17 );
18}
19
20export default TodoList;
To Do Item Component
1import React from 'react';
2
3function TodoItem({ todo, toggleTodo, deleteTodo, setEditingTodo }) {
4 return (
5 <li>
6 <input
7 type="checkbox"
8 checked={todo.completed}
9 onChange={() => toggleTodo(todo.id)}
10 />
11 <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
12 {todo.text}
13 </span>
14 <button onClick={() => setEditingTodo(todo)}>Edit</button>
15 <button onClick={() => deleteTodo(todo.id)}>Delete</button>
16 </li>
17 );
18}
19
20export default TodoItem;
Authentication Component
1import React, { useState, useEffect } from 'react';
2import axios from 'axios';
3
4function Auth({ onLogin, onLogout, isAuthenticated, user }) {
5 const [username, setUsername] = useState('');
6 const [password, setPassword] = useState('');
7 const [error, setError] = useState('');
8
9 const handleLogin = async (e) => {
10 e.preventDefault();
11 try {
12 await axios.post('/auth/login', { username, password }, { withCredentials: true });
13 onLogin(); // Notify parent component
14 } catch (err) {
15 setError('Invalid username or password');
16 }
17 };
18
19 const handleLogout = async () => {
20 try {
21 await axios.post('/auth/logout', {}, { withCredentials: true });
22 onLogout(); // Notify parent component
23 } catch (err) {
24 setError('Logout error');
25 }
26 };
27
28 useEffect(() => {
29 const fetchStatus = async () => {
30 try {
31 const response = await axios.get('/auth/status', { withCredentials: true });
32 if (response.data.authenticated) {
33 onLogin(response.data.user);
34 } else {
35 onLogout();
36 }
37 } catch (err) {
38 console.error('Error checking authentication status', err);
39 }
40 };
41
42 fetchStatus();
43 }, [onLogin, onLogout]);
44
45 return (
46 <div>
47 {isAuthenticated ? (
48 <div>
49 <p>Welcome, {user.username}</p>
50 <button onClick={handleLogout}>Logout</button>
51 </div>
52 ) : (
53 <form onSubmit={handleLogin}>
54 <input
55 type="text"
56 value={username}
57 onChange={(e) => setUsername(e.target.value)}
58 placeholder="Username"
59 />
60 <input
61 type="password"
62 value={password}
63 onChange={(e) => setPassword(e.target.value)}
64 placeholder="Password"
65 />
66 <button type="submit">Login</button>
67 {error && <p>{error}</p>}
68 </form>
69 )}
70 </div>
71 );
72}
73
74export default Auth;
State Management using Redux
Install Dependencies!npm install @reduxjs/toolkit react-redux
1//features/todos/todoSlice.js
2
3import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
4import axios from 'axios';
5
6// Async thunks
7export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
8 const response = await axios.get('/api/todos');
9 return response.data;
10});
11
12export const addTodo = createAsyncThunk('todos/addTodo', async (text) => {
13 const response = await axios.post('/api/todos', { text });
14 return response.data;
15});
16
17export const updateTodo = createAsyncThunk('todos/updateTodo', async (todo) => {
18 const response = await axios.put(`/api/todos/${todo.id}`, todo);
19 return response.data;
20});
21
22export const deleteTodo = createAsyncThunk('todos/deleteTodo', async (id) => {
23 await axios.delete(`/api/todos/${id}`);
24 return id;
25});
26
27// Todo slice
28const todoSlice = createSlice({
29 name: 'todos',
30 initialState: {
31 todos: [],
32 status: 'idle',
33 error: null
34 },
35 reducers: {},
36 extraReducers: (builder) => {
37 builder
38 .addCase(fetchTodos.pending, (state) => {
39 state.status = 'loading';
40 })
41 .addCase(fetchTodos.fulfilled, (state, action) => {
42 state.status = 'succeeded';
43 state.todos = action.payload;
44 })
45 .addCase(fetchTodos.rejected, (state, action) => {
46 state.status = 'failed';
47 state.error = action.error.message;
48 })
49 .addCase(addTodo.fulfilled, (state, action) => {
50 state.todos.push(action.payload);
51 })
52 .addCase(updateTodo.fulfilled, (state, action) => {
53 const index = state.todos.findIndex(todo => todo.id === action.payload.id);
54 if (index !== -1) {
55 state.todos[index] = action.payload;
56 }
57 })
58 .addCase(deleteTodo.fulfilled, (state, action) => {
59 state.todos = state.todos.filter(todo => todo.id !== action.payload);
60 });
61 }
62});
63
64export default todoSlice.reducer;
1//file - app/store.js
2
3import { configureStore } from '@reduxjs/toolkit';
4import todoReducer from '../features/todos/todoSlice';
5
6const store = configureStore({
7 reducer: {
8 todos: todoReducer
9 }
10});
11
12export default store;
1import React from 'react';
2import ReactDOM from 'react-dom/client';
3import { Provider } from 'react-redux';
4import store from './app/store';
5import App from './App';
6
7ReactDOM.createRoot(document.getElementById('root')).render(
8 <Provider store={store}>
9 <App />
10 </Provider>
11);
1//file - App.js
2
3import React, { useEffect, useState } from 'react';
4import { useSelector, useDispatch } from 'react-redux';
5import { fetchTodos, addTodo, updateTodo, deleteTodo } from './features/todos/todoSlice';
6import TodoList from './components/TodoList';
7import TodoForm from './components/TodoForm';
8import Filter from './components/Filter';
9import Auth from './components/Auth';
10
11function App() {
12 const dispatch = useDispatch();
13 const todos = useSelector((state) => state.todos.todos);
14 const status = useSelector((state) => state.todos.status);
15 const [filter, setFilter] = useState('all');
16 const [editingTodo, setEditingTodo] = useState(null);
17 const [isAuthenticated, setIsAuthenticated] = useState(false);
18 const [user, setUser] = useState(null);
19
20 useEffect(() => {
21 if (status === 'idle') {
22 dispatch(fetchTodos());
23 }
24 }, [status, dispatch]);
25
26 const handleAddTodo = async (text) => {
27 await dispatch(addTodo(text));
28 };
29
30 const handleEditTodo = async (updatedTodo) => {
31 await dispatch(updateTodo(updatedTodo));
32 setEditingTodo(null);
33 };
34
35 const handleToggleTodo = async (id) => {
36 const todo = todos.find(todo => todo.id === id);
37 const updatedTodo = { ...todo, completed: !todo.completed };
38 await dispatch(updateTodo(updatedTodo));
39 };
40
41 const handleDeleteTodo = async (id) => {
42 await dispatch(deleteTodo(id));
43 };
44
45 const filteredTodos = todos.filter(todo => {
46 if (filter === 'completed') return todo.completed;
47 if (filter === 'active') return !todo.completed;
48 return true;
49 });
50
51 const handleLogin = (userData) => {
52 setIsAuthenticated(true);
53 setUser(userData);
54 };
55
56 const handleLogout = () => {
57 setIsAuthenticated(false);
58 setUser(null);
59 };
60
61 return (
62 <div>
63 <h1>Todo App</h1>
64 <Auth onLogin={handleLogin} onLogout={handleLogout} isAuthenticated={isAuthenticated} user={user} />
65 {isAuthenticated && (
66 <>
67 <TodoForm addTodo={handleAddTodo} editTodo={handleEditTodo} currentTodo={editingTodo} />
68 <Filter setFilter={setFilter} />
69 <TodoList
70 todos={filteredTodos}
71 toggleTodo={handleToggleTodo}
72 deleteTodo={handleDeleteTodo}
73 setEditingTodo={setEditingTodo}
74 />
75 </>
76 )}
77 </div>
78 );
79}
80
81export default App;
1//file - TodoForm.js
2import React, { useState, useEffect } from 'react';
3
4function TodoForm({ addTodo, editTodo, currentTodo }) {
5 const [input, setInput] = useState('');
6
7 useEffect(() => {
8 if (currentTodo) {
9 setInput(currentTodo.text);
10 } else {
11 setInput('');
12 }
13 }, [currentTodo]);
14
15 const handleSubmit = (e) => {
16 e.preventDefault();
17 if (input.trim()) {
18 if (currentTodo) {
19 editTodo({
20 ...currentTodo,
21 text: input
22 });
23 } else {
24 addTodo(input);
25 }
26 setInput('');
27 }
28 };
29
30 return (
31 <form onSubmit={handleSubmit}>
32 <input
33 type="text"
34 value={input}
35 onChange={(e) => setInput(e.target.value)}
36 placeholder={currentTodo ? "Edit todo" : "Add a new todo"}
37 />
38 <button type="submit">{currentTodo ? "Update Todo" : "Add Todo"}</button>
39 </form>
40 );
41}
42
43export default TodoForm;
1import React from 'react';
2import TodoItem from './TodoItem';
3
4function TodoList({ todos, toggleTodo, deleteTodo, setEditingTodo }) {
5 return (
6 <ul>
7 {todos.map(todo => (
8 <TodoItem
9 key={todo.id}
10 todo={todo}
11 toggleTodo={toggleTodo}
12 deleteTodo={deleteTodo}
13 setEditingTodo={setEditingTodo}
14 />
15 ))}
16 </ul>
17 );
18}
19
20export default TodoList;
1import React from 'react';
2
3function TodoItem({ todo, toggleTodo, deleteTodo, setEditingTodo }) {
4 return (
5 <li>
6 <input
7 type="checkbox"
8 checked={todo.completed}
9 onChange={() => toggleTodo(todo.id)}
10 />
11 <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
12 {todo.text}
13 </span>
14 <button onClick={() => setEditingTodo(todo)}>Edit</button>
15 <button onClick={() => deleteTodo(todo.id)}>Delete</button>
16 </li>
17 );
18}
19
20export default TodoItem;
1import React from 'react';
2
3function Filter({ setFilter }) {
4 return (
5 <div>
6 <button onClick={() => setFilter('all')}>All</button>
7 <button onClick={() => setFilter('active')}>Active</button>
8 <button onClick={() => setFilter('completed')}>Completed</button>
9 </div>
10 );
11}
12
13export default Filter;
Going Live
Deploy to Vercel
Install dependency.npm install -g vercel
npm run build
1vercel login
2cd /path/to/your/react-app
3vercel
Custom Domain
If you have a custom domain, you can add it to your Vercel project under the "Domains" tab.Checking Logs and Metrics
You can look at logs of your in app in vercel dashboard!Securing App
Sanitizing Forms
Sanitizing form inputs in React is crucial for protecting your application from security vulnerabilities such as Cross-Site Scripting (XSS) and ensuring that the data submitted by users is clean and formatted correctly.
Install dompurify and validator packages!1npm install dompurify
2npm install validator
1import React, { useState } from 'react';
2import DOMPurify from 'dompurify';
3import validator from 'validator';
4
5const Form = () => {
6 const [input, setInput] = useState('');
7 const [email, setEmail] = useState('');
8 const [error, setError] = useState('');
9
10 const handleInputChange = (e) => {
11 const rawInput = e.target.value;
12
13 // Trim and remove special characters
14 const sanitizedInput = rawInput.trim().replace(/[^ws]/gi, '');
15 setInput(sanitizedInput);
16 };
17
18 const handleEmailChange = (e) => {
19 const rawEmail = e.target.value;
20
21 // Escape HTML and validate email
22 const sanitizedEmail = DOMPurify.sanitize(rawEmail);
23 if (validator.isEmail(sanitizedEmail)) {
24 setEmail(sanitizedEmail);
25 setError('');
26 } else {
27 setError('Invalid email address');
28 }
29 };
30
31 const handleSubmit = (e) => {
32 e.preventDefault();
33 // Proceed with the sanitized and validated input
34 console.log('Sanitized Input:', input);
35 console.log('Sanitized and Validated Email:', email);
36 };
37
38 return (
39 <form onSubmit={handleSubmit}>
40 <div>
41 <label>Text Input:</label>
42 <input
43 type="text"
44 value={input}
45 onChange={handleInputChange}
46 />
47 </div>
48 <div>
49 <label>Email:</label>
50 <input
51 type="email"
52 value={email}
53 onChange={handleEmailChange}
54 />
55 {error && <p style={{ color: 'red' }}>{error}</p>}
56 </div>
57 <button type="submit">Submit</button>
58 </form>
59 );
60};
61
62export default Form;
Stopping Bots using Captcha
Turnstile is a CAPTCHA alternative provided by Cloudflare, designed to verify that a user is human without requiring them to complete traditional CAPTCHA challenges. Integrating Turnstile into a React form involves several steps, including setting up Turnstile on the server-side and then integrating it into your React application.npm install @marsidev/react-turnstile
1import React, { useState } from 'react';
2import Turnstile from '@marsidev/react-turnstile';
3
4const MyForm = () => {
5 const [captchaToken, setCaptchaToken] = useState('');
6
7 const handleSubmit = async (e) => {
8 e.preventDefault();
9
10 // Submit form data along with the Turnstile token
11 const formData = {
12 // Your form data
13 captchaToken,
14 };
15
16 // Example: POST formData to your server
17 const response = await fetch('/api/submit-form', {
18 method: 'POST',
19 headers: {
20 'Content-Type': 'application/json',
21 },
22 body: JSON.stringify(formData),
23 });
24
25 const result = await response.json();
26 console.log(result);
27 };
28
29 return (
30 <form onSubmit={handleSubmit}>
31 <div>
32 <label>Your Name:</label>
33 <input type="text" name="name" required />
34 </div>
35 <div>
36 <label>Your Email:</label>
37 <input type="email" name="email" required />
38 </div>
39
40 {/* Turnstile CAPTCHA */}
41 <Turnstile
42 sitekey="your-site-key-here"
43 onSuccess={(token) => setCaptchaToken(token)}
44 onError={() => setCaptchaToken('')}
45 />
46
47 <button type="submit" disabled={!captchaToken}>Submit</button>
48 </form>
49 );
50};
51
52export default MyForm;
Rate Limiting
Rate limiting is a technique used to control the number of requests a user or client can make to your server within a specific period. This helps to prevent abuse, such as DDoS attacks, brute-force login attempts, or API misuse.
Install express-rate-limit package!npm install express-rate-limit
1const express = require('express');
2const rateLimit = require('express-rate-limit');
3
4const app = express();
5
6// Apply rate limiting to all requests
7const limiter = rateLimit({
8 windowMs: 15 * 60 * 1000, // 15 minutes
9 max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
10 message: 'Too many requests from this IP, please try again after 15 minutes',
11});
12
13// Apply the rate limiter to all requests
14app.use(limiter);
15
16app.get('/', (req, res) => {
17 res.send('Hello, world!');
18});
19
20app.listen(3000, () => {
21 console.log('Server is running on port 3000');
22});
1// Apply to login route only
2const loginLimiter = rateLimit({
3 windowMs: 15 * 60 * 1000, // 15 minutes
4 max: 5, // Limit each IP to 5 login requests per `window`
5 message: 'Too many login attempts, please try again after 15 minutes',
6});
7
8app.post('/login', loginLimiter, (req, res) => {
9 // Handle login
10});
WAF - Web Application Firewall
A Web Application Firewall (WAF) works by filtering and monitoring HTTP/HTTPS traffic between a web application and the internet to protect against various web-based threats. WAFs inspect incoming HTTP/HTTPS requests to identify and block malicious traffic. They check requests against predefined rules or patterns to detect potentially harmful content. WAFs use signatures and patterns to identify known attack vectors, such as SQL injection strings or XSS payloads.
Some examples- Block requests containing SQL keywords such as SELECT, UNION, or DROP in URL parameters or request bodies.
- Block requests containing script tags (script) or JavaScript events (onerror, onclick) in user input.
- Block uploads of executable files (.exe, .php) or files larger than a specified size.
- Block requests using methods like TRACE or DELETE if not required by the application.
- Block known malicious IP addresses or allow only certain trusted IP ranges.
- Block requests with suspicious patterns, such as rapid request rates or known bot signatures.
- Allow a maximum of 100 requests per IP address per minute.
Scaling App
Using middleware
- Rate Limiting is usually implemented in middleware!
- Cache responses to reduce the load on your application and database by serving cached content for frequently accessed requests.
- Reduce the size of the response data sent to clients, improving load times and reducing bandwidth usage.
- Manage user authentication and authorization, ensuring that only authorized users can access certain resources.
- Log requests and responses for monitoring and debugging purposes.
- Protect your application from common security threats, such as cross-site scripting (XSS) and clickjacking, which can impact performance and reliability.
1const helmet = require('helmet'); 2 3// Apply security headers to all responses 4app.use(helmet());
Horizontal Scaling
- Containerization and K8s
- Load Balancing
- Auto Scaling
Vertical Scaling
- Upgrade Instance Size
- Database Optimization
Caching
- In-Memory Caching - Redis/Memcached
- HTTP Caching - Set appropriate cache headers (e.g., Cache-Control, Expires) to enable client-side caching of static resources
- Content Delivery Network (CDN)
Asynchronous Processing
- Background jobs - Use job queues like Bull or Kue to handle background tasks asynchronously. Implement worker processes or microservices to handle long-running tasks separately from your main application.
- Use message brokers (RabbitMQ/Kafka) to manage communication between different components of your application and handle distributed tasks.
Monitoring and logging
- Use tools like Prometheus, Grafana, or New Relic to monitor application performance, resource usage, and response times.
- Use centralized logging systems like ELK Stack (Elasticsearch, Logstash, Kibana) or Loki to aggregate and analyze logs from multiple instances.