Introduction
In our last lesson, you built the GET /api/items endpoint, which successfully retrieves all documents from your database using Model.find(). This is the "Read" part of CRUD, but for an entire collection. Now, we'll focus on a more specific task: retrieving a single item by its unique ID.
This lesson addresses a core requirement for your exam: implementing a GET route to fetch a specific resource. We will create the GET /api/items/:id endpoint. You'll learn how to capture dynamic parts of a URL (route parameters), use the Mongoose Model.findById() method, and, crucially, handle the different possible outcomes:
- Success (200): The item is found and returned.
- Not Found (404): The ID has a valid format, but no item with that ID exists in the database.
- Bad Request (400): The client provides an ID in an invalid format.
By the end of this lesson, you'll have a robust endpoint for fetching individual items, a critical piece of any REST API.
1. Route Parameters and Model.findById()
To fetch a specific item, the client needs to tell us which item they want. The standard RESTful way to do this is by including the item's unique identifier in the URL path. Express allows us to define routes that capture these dynamic segments.
A route path like /api/items/:id uses a route parameter denoted by the colon (:). When a request comes in to a URL like /api/items/60d21b4667d0d8992e610c85, Express captures the ID part and makes it available in the req.params object, specifically as req.params.id.
Once we have the id, we can use the Mongoose method designed for this exact purpose: Model.findById(id).
Model.findById(id): This method takes a single argument, the_idof the document you want to find.- Return Value:
- If a document with that
_idis found, it returns the document object. - If no document is found, it returns
null. - If the
idstring is not a valid MongoDB ObjectId format, it throws aCastError.
- If a document with that
Your job is to handle all three of these possibilities correctly.
2. The Implementation Pattern
The most direct way to implement this route is to use an async handler with a try...catch block. This allows you to await the result from findById and gracefully handle any errors that occur.
For a clear, concise, and production-ready implementation pattern, review the following code snippet from an article by OneUptime.
How to Use MongoDB with Express and Mongoose - OneUptime
The article 'How to Use MongoDB with Express and Mongoose' provides a perfect code example for a 'Get single user by ID' route. It demonstrates exactly how to use findById, check for a null result to return a 404, and handle invalid ID formats with a 400 status.
In the 'Read' section of the code block, focus on the second route handler: router.get('/:id', async (req, res) => { ... });. Pay close attention to the if (!user) check and the catch block's logic for handling error.name === 'CastError'.
As you saw in the article, the logic is straightforward:
- Attempt to find the item by its ID.
- If it returns
null(!userin the example), the item doesn't exist. You must stop execution and return a404 Not Found. - If it's found, send it back with a
200 OK. - If the database query itself fails (e.g.,
CastErrorfor a bad ID), thecatchblock is executed. Here, you inspect the error to decide if it's a client mistake (400 Bad Request) or a true server issue (500 Internal Server Error).
The pattern of handling an invalid ObjectId format specifically is a hallmark of a robust API. You'll see this reflected in test suites, which often have dedicated tests for this scenario.

3. A Reusable Middleware Pattern (Advanced)
While the direct implementation is fine, in larger applications you often need to fetch a resource by ID in multiple routes (GET /:id, PUT /:id, DELETE /:id). To avoid repeating code, you can extract the logic into a reusable middleware function.
Watch this segment from the Web Dev Simplified video to see this powerful pattern in action.
Build A REST API With Node.js, Express, & MongoDB - Quick
In 'Build A REST API With Node.js, Express, & MongoDB - Quick', the presenter creates a middleware function called getSubscriber that finds a resource by ID and attaches it to the response object. This is a very clean and professional way to structure your code for reusability.
Watch from 20:20 to 23:21. Focus on how the getSubscriber middleware function is created. It takes req, res, and next as arguments. It finds the subscriber, handles the 404 case, attaches the result to res, and then calls next() to pass control to the actual route handler.
For the exam, either the direct implementation or the middleware pattern is acceptable. The middleware pattern shows a deeper understanding of Express and can make your code cleaner if you need to perform the same lookup for multiple endpoints.
4. Your Turn: Implement the Route
Now, let's add the GET /api/items/:id route to your item.routes.js file. We will use the direct implementation pattern, as it's the most straightforward for a single route.
Action: Open src/routes/item.routes.js and add the following code below your existing GET / route.
// src/routes/item.routes.js
const express = require('express');
const router = express.Router();
const Item = require('../models/item.model.js');
const mongoose = require('mongoose'); // Import mongoose
// @route GET /api/items
// @desc Get all items
// @access Public
router.get('/', async (req, res) => {
try {
const items = await Item.find();
res.status(200).json(items);
} catch (error) {
res.status(500).json({ message: "Failed to retrieve items", error: error.message });
}
});
// @route GET /api/items/:id
// @desc Get a single item by ID
// @access Public
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
// Check if the ID is a valid ObjectId format before querying
if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).json({ message: 'Invalid item ID format' });
}
const item = await Item.findById(id);
if (!item) {
// The ID is valid, but no item was found
return res.status(404).json({ message: 'Item not found' });
}
// Item was found
res.status(200).json(item);
} catch (error) {
// For other unexpected errors
res.status(500).json({ message: 'Server error while retrieving item', error: error.message });
}
});
module.exports = router;
Code Refinement:
In this implementation, I've used mongoose.Types.ObjectId.isValid(id) as an explicit check. This is slightly different from catching the CastError but achieves the same result and can be more readable. It immediately rejects improperly formatted IDs with a 400 Bad Request before even attempting a database query.
5. Testing Your Endpoint
It's crucial to test all three scenarios.
-
Make sure your server is running (
npm run dev). -
Test the Success Case (200 OK):
- First, send a
GETrequest tohttp://localhost:5001/api/itemsto get a list of your items. - Copy the
_idfrom one of the items. - Now, send a new
GETrequest tohttp://localhost:5001/api/items/YOUR_COPIED_ID. - Result: You should get a
200 OKstatus and the JSON object for that single item.
- First, send a
-
Test the Not Found Case (404 Not Found):
- Take a valid
_idand change its last character (e.g.,...c85becomes...c86). The format is still valid, but it won't exist in the database. - Send a
GETrequest tohttp://localhost:5001/api/items/YOUR_MODIFIED_ID. - Result: You should get a
404 Not Foundstatus and the JSON message{ "message": "Item not found" }.
- Take a valid
-
Test the Bad Request Case (400 Bad Request):
- Send a
GETrequest using an obviously invalid ID. - URL:
http://localhost:5001/api/items/123. - Result: You should get a
400 Bad Requeststatus and the JSON message{ "message": "Invalid item ID format" }.
- Send a
Conclusion
You have now implemented and tested a robust endpoint for retrieving a single resource by its ID. This is a fundamental skill that will be heavily utilized in your exam and in any backend project.
Key Takeaways:
- Route parameters like
:idare used to capture dynamic values from the URL, accessible viareq.params. Model.findById(id)is the primary tool for fetching a document by its unique ID.- A production-ready implementation must handle three distinct cases:
- Success (200): The resource is found.
- Not Found (404): The ID is valid, but the resource doesn't exist.
- Bad Request (400): The ID itself is malformed.
Next Up:
You've mastered reading one and all items. The next logical step in our CRUD journey is creating new ones. In the following lesson, you will implement the POST /api/items route, where you'll learn to handle incoming request bodies and use Model.create() to add new documents to your database.