In this tutorial, we'll build a simple full-stack feature using the MERN stack (MongoDB, Express, React, Node.js) that demonstrates how to work with related models—specifically, a Post that can belong to multiple Categories.
A common challenge when working with related data in MongoDB is editing and updating references between documents. For example, when editing a Post that is related to multiple Category documents, how do we display the existing relationships, allow the user to edit them in the front-end, and then persist those changes back to the database?
We’ll break this down step-by-step:
- Creating related models with Mongoose
- Using .populate() to fetch complete referenced documents
- Building Express routes to fetch and update a post
- Creating a React form that allows selecting multiple categories
- Submitting only the necessary data to the backend
This guide focuses on clarity and simplicity—no CSS, no extra libraries—just the core logic you need to manage related data efficiently in a real-world app.
Let’s get started!
I. Mongoose Models
models/Category.js
        
        
          
            
          
          
        
        const mongoose = require('mongoose');
const CategorySchema = new mongoose.Schema({
  title: { type: String, required: true }
});
module.exports = mongoose.model('Category', CategorySchema);
      models/Post.js
        
        
          
            
          
          
        
        const mongoose = require('mongoose');
const PostSchema = new mongoose.Schema({
  title: { type: String, required: true },
  description: { type: String },
  categories: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Category' }]
});
module.exports = mongoose.model('Post', PostSchema);
      Note: One post can have multiple categories.
II. Route Controllers
1. GET /api/post/:id – Fetch a post along with its associated categories
        
        
          
            
          
          
        
        router.get('/api/posts/:id', async (req, res) => {
  try {
    const post = await Post.findById(req.params.id).populate('categories');
    if (!post) return res.status(404).json({ message: 'Post not found' });
    res.json(post);
  } catch (error) {
    res.status(500).json({ message: 'Server error', error });
  }
});
      2. POST /api/post/:id – Update a post and its category relationships
        
        
          
            
          
          
        
        
router.post('/api/posts/:id', async (req, res) => {
  try {
    const { title, description, categories } = req.body;
    const updatedPost = await Post.findByIdAndUpdate(
      req.params.id,
      {
        title,
        description,
        categories: categories ? categories.map(id => id) : []
      },
      { new: true }
    ).populate('categories');
    if (!updatedPost) return res.status(404).json({ message: 'Post not found' });
    res.json(updatedPost);
  } catch (error) {
    res.status(500).json({ message: 'Server error', error });
  }
});
      Here we expect req.body.categories to be an array of category IDs from the frontend, so we sanitize it with .map(id => id).
III. The view
1. State variables
        
        
          
            
          
          
        
        const [formData, setFormData] = useState({
  title: '',
  description: '',
  categories: [],
});
const [allCategories, setAllCategories] = useState([]);
      2. Fetching the data
        
        
          
            
          
          
        
        useEffect(() => {
    const fetchData = async () => {
      const postRes = await fetch(`/api/posts/${postId}`);
      const post = await postRes.json();
      const categoriesRes = await fetch('/api/categories');
      const categories = await categoriesRes.json();
      setFormData({
        title: post.title,
        description: post.description,
        categories: post.categories.map(c => c._id),
      });
      setAllCategories(categories);
    };
    fetchData();
  }, [postId]);
      3. Handling Post, Categories and the form submission
        
        
          
            
          
          
        
        const handleInputChange = e => {
    setFormData({ ...formData, [e.target.name]: e.target.value });
  };
  const handleCategoryToggle = (categoryId) => {
    const alreadySelected = formData.categories.includes(categoryId);
    const updatedCategories = alreadySelected
      ? formData.categories.filter(id => id !== categoryId)
      : [...formData.categories, categoryId];
    setFormData(prevData => ({
      ...prevData,
      categories: updatedCategories,
    }));
  };
  const handleSubmit = async e => {
    e.preventDefault();
    const payload = {
      title: formData.title,
      description: formData.description,
      categories: formData.categories,
    };
    const res = await fetch(`/api/posts/${postId}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
    const updatedPost = await res.json();
    alert('Post updated!');
    console.log(updatedPost);
  };
      4. And last but not least, the view:
        
        
          
            
          
          
        
        return (
    <form onSubmit={handleSubmit}>
      <h1>Edit Post</h1>
      <div>
        <label>Title:</label>
        <input
          name="title"
          value={formData.title}
          onChange={handleInputChange}
        />
      </div>
      <div>
        <label>Description:</label>
        <textarea
          name="description"
          value={formData.description}
          onChange={handleInputChange}
        />
      </div>
      <div>
        <h3>Select Categories:</h3>
        {allCategories.map(category => (
          <label key={category._id}>
            <input
              type="checkbox"
              checked={formData.categories.includes(category._id)}
              onChange={() => handleCategoryToggle(category._id)}
            />
            {category.title}
          </label>
        ))}
      </div>
      <button type="submit">Save</button>
    </form>
  );
      Final thoughts
In this post, we walked through:
- Creating related models with Mongoose
- Using .populate() to fetch complete referenced documents
- Building Express routes to fetch and update a post
- Creating a React form that allows selecting multiple categories
- Submitting only the necessary data to the backend