Building a Simple MERN Stack Application from Scratch
The MERN stack—MongoDB, Express, React, and Node.js—is a powerful and popular stack for building full-stack JavaScript applications. It allows developers to use a single language, JavaScript, across the entire stack, which streamlines development and makes learning easier. In this guide, we’ll go through building a simple MERN stack application from scratch.
Introduction
Creating a full-stack application from scratch might seem daunting, but with the MERN stack, you can set up and structure your project quickly, using JavaScript for both front-end and back-end code. This guide will walk you through building a basic CRUD application using MongoDB as the database, Express as the backend framework, React for the front end, and Node.js as the server environment.
Prerequisites
Ensure you have Node.js and MongoDB installed on your machine. Basic knowledge of JavaScript and the fundamentals of these frameworks is helpful but not required.
Step 1: Setting Up the Backend with Node.js and Express
Initialize a new Node.js project in your chosen directory:
bash
mkdir mern-tutorial
cd mern-tutorial
npm init -y
Install Express and Mongoose, which we’ll use to connect to MongoDB and manage our API routes.
tsx
npm install express mongoose cors
Set up the Express server in a new file called server.ts
:
tsx
import express, { Request, Response } from 'express';
import mongoose from 'mongoose';
import cors from 'cors';
const app = express();
const PORT = 5000;
app.use(cors());
app.use(express.json());
mongoose.connect('mongodb://localhost:27017/mernapp', {
useNewUrlParser: true,
useUnifiedTopology: true,
}).then(() => console.log('Connected to MongoDB'));
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Define a Model for your data using Mongoose. Let’s say we’re creating a simple to-do app:
tsx
import mongoose, { Document } from 'mongoose';
interface ITodo extends Document {
title: string;
completed: boolean;
}
const TodoSchema = new mongoose.Schema({
title: { type: String, required: true },
completed: { type: Boolean, default: false },
});
export const Todo = mongoose.model<ITodo>('Todo', TodoSchema);
Set up routes to create, read, update, and delete to-do items. Add the following routes to server.ts
:
tsx
app.post('/todos', async (req: Request, res: Response) => {
const { title } = req.body;
const newTodo = new Todo({ title });
await newTodo.save();
res.json(newTodo);
});
app.get('/todos', async (req: Request, res: Response) => {
const todos = await Todo.find();
res.json(todos);
});
app.put('/todos/:id', async (req: Request, res: Response) => {
const { id } = req.params;
const updatedTodo = await Todo.findByIdAndUpdate(id, req.body, { new: true });
res.json(updatedTodo);
});
app.delete('/todos/:id', async (req: Request, res: Response) => {
const { id } = req.params;
await Todo.findByIdAndDelete(id);
res.json({ message: 'Todo deleted' });
});
Step 2: Setting Up the Frontend with React
Create a new React project in a separate directory using Create React App:
scss
npx create-react-app client --template typescript
cd client
npm install axios
Create a service for API calls using Axios in a new file called api.ts
:
tsx
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:5000',
});
export const fetchTodos = () => api.get('/todos');
export const createTodo = (todo: { title: string }) => api.post('/todos', todo);
export const updateTodo = (id: string, updatedTodo: { completed: boolean }) => api.put(`/todos/${id}`, updatedTodo);
export const deleteTodo = (id: string) => api.delete(`/todos/${id}`);
Build a simple component to display the list of to-dos and allow for creating new to-dos. In App.tsx
:
tsx
import React, { useEffect, useState } from 'react';
import { fetchTodos, createTodo, updateTodo, deleteTodo } from './api';
interface Todo {
_id: string;
title: string;
completed: boolean;
}
const App: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [title, setTitle] = useState('');
useEffect(() => {
const getTodos = async () => {
const { data } = await fetchTodos();
setTodos(data);
};
getTodos();
}, []);
const addTodo = async () => {
const { data } = await createTodo({ title });
setTodos([...todos, data]);
setTitle('');
};
const toggleComplete = async (id: string) => {
const todo = todos.find(todo => todo._id === id);
if (todo) {
const { data } = await updateTodo(id, { completed: !todo.completed });
setTodos(todos.map(todo => (todo._id === id ? data : todo)));
}
};
const removeTodo = async (id: string) => {
await deleteTodo(id);
setTodos(todos.filter(todo => todo._id !== id));
};
return (
<div>
<h1>To-Do List</h1>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="New to-do"
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo._id}>
<span
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => toggleComplete(todo._id)}
>
{todo.title}
</span>
<button onClick={() => removeTodo(todo._id)}>Delete</button>
</li>
))}
</ul>
</div>
);
};
export default App;
Step 3: Connecting Frontend and Backend
Start both servers:
Run
npm start
in theserver
folder to start the backend.Run
npm start
in theclient
folder to start the frontend.
Enable CORS in the backend (already added in Step 1) to allow the frontend to make requests.
Test your application: You should be able to add, complete, and delete to-do items, and have the data persist through page refreshes.
Conclusion
Building a simple MERN stack application gives you a solid foundation for creating full-stack applications. This stack's versatility allows you to scale your projects while using a single language, JavaScript. Try adding more features like authentication, pagination, or search functionality to further improve and expand your app!