We're going to build a complete API for managing a nature database, focusing on trees and insects. Think of this like building a digital catalog for a national park. Just as a park ranger needs to track information about the park's natural features, we'll create routes to manage our database of trees and insects.
Let's start with the fundamental operations of retrieving data. These are like looking up entries in our park ranger's guidebook:
// First, we need to import our Tree model
const { Tree } = require('../models');
// Route to list all trees
router.get('/', async (req, res) => {
// Find all trees, but only select specific attributes
const trees = await Tree.findAll({
attributes: ['heightFt', 'tree', 'id'],
order: [['heightFt', 'DESC']] // Order by height, tallest first
});
res.json(trees);
});
This route is like having a catalog where we can look up all the trees in our database. Notice how we:
1. Specify exactly which attributes we want (like focusing on specific details in our guidebook)
2. Order the results (like organizing our guidebook by tree height)
Now let's learn how to add new entries to our database. This is like adding a newly discovered tree to our guidebook:
// Route to create a new tree
router.post('/', async (req, res) => {
try {
// Extract the tree information from the request
const { name, location, height, size } = req.body;
// Create a new tree record
const tree = await Tree.create({
tree: name, // Notice how we map the incoming names
location: location, // to our database field names
heightFt: height,
groundCircumferenceFt: size
});
// Send back a success response
res.json({
status: "success",
message: "Successfully created new tree",
data: tree
});
} catch (error) {
// If something goes wrong, handle it gracefully
next({
status: "error",
message: "Could not create tree",
details: error.message
});
}
});
Sometimes we need to remove records from our database. Let's build this functionality carefully:
// Route to delete a tree by ID
router.delete('/:id', async (req, res, next) => {
try {
// First find the tree
const tree = await Tree.findByPk(req.params.id);
// If we can't find it, return a friendly error
if (!tree) {
next({
status: "not-found",
message: `Could not remove tree ${req.params.id}`,
details: "Tree not found"
});
return;
}
// Store the tree data before deletion
const deletedTree = tree.toJSON();
// Remove the tree from the database
await tree.destroy();
// Send back a success response with the deleted tree's data
res.json({
status: "success",
message: "Successfully removed tree",
data: deletedTree
});
} catch (error) {
next(error);
}
});
Now let's implement the ability to update existing records. This is like updating the measurements of a tree after a new survey:
// Route to update a tree
router.put('/:id', async (req, res, next) => {
try {
// Validate that IDs match if both are provided
if (req.body.id && parseInt(req.params.id) !== parseInt(req.body.id)) {
throw {
status: "error",
message: "Could not update tree",
details: `${req.params.id} does not match ${req.body.id}`
};
}
// Find the tree we want to update
const tree = await Tree.findByPk(req.params.id);
// Handle tree not found
if (!tree) {
throw {
status: "not-found",
message: `Could not update tree ${req.params.id}`,
details: "Tree not found"
};
}
// Update the tree's attributes
const { name, location, height, size } = req.body;
if (name) tree.tree = name;
if (location) tree.location = location;
if (height) tree.heightFt = height;
if (size) tree.groundCircumferenceFt = size;
// Save the changes
await tree.save();
// Send back a success response
res.json({
status: "success",
message: "Successfully updated tree",
data: tree
});
} catch (error) {
next(error);
}
});
Now we'll learn how to establish relationships between our models. This is like recording which insects are found near which trees:
// In our Tree model
static associate(models) {
Tree.belongsToMany(models.Insect, {
through: 'InsectTree',
foreignKey: 'treeId',
otherKey: 'insectId'
});
}
// In our Insect model
static associate(models) {
Insect.belongsToMany(models.Tree, {
through: 'InsectTree',
foreignKey: 'insectId',
otherKey: 'treeId'
});
}
Finally, let's learn how to retrieve data that includes related records. We can do this in two ways:
// Get all trees with their associated insects
router.get('/trees-insects', async (req, res) => {
const treesWithInsects = await Tree.findAll({
include: [{
model: Insect,
attributes: ['id', 'name'],
through: { attributes: [] } // Hide join table data
}],
order: [[Insect, 'name', 'ASC']]
});
res.json(treesWithInsects);
});
// Get insects and load their trees later
router.get('/insects-trees', async (req, res) => {
const insects = await Insect.findAll();
// Load the related trees for each insect
for (let insect of insects) {
insect.trees = await insect.getTrees({
attributes: ['id', 'tree']
});
}
res.json(insects);
});
Throughout our routes, we've implemented careful error handling. This is crucial for building robust APIs:
// Example of comprehensive error handling
try {
// Our database operations here
} catch (error) {
// Handle specific types of errors
if (error.name === 'SequelizeValidationError') {
next({
status: "error",
message: "Validation error",
details: error.errors.map(e => e.message)
});
} else if (error.name === 'SequelizeUniqueConstraintError') {
next({
status: "error",
message: "Duplicate entry",
details: "This record already exists"
});
} else {
// Handle unexpected errors
next({
status: "error",
message: "An unexpected error occurred",
details: error.message
});
}
}
For each route we've created, we should test different scenarios:
curl http://localhost:8000/trees
curl -X POST http://localhost:8000/trees \
-H "Content-Type: application/json" \
-d '{
"name": "Giant Sequoia",
"location": "Sequoia National Park",
"height": 275.3,
"size": 102.6
}'
curl -X PUT http://localhost:8000/trees/1 \
-H "Content-Type: application/json" \
-d '{
"height": 276.1
}'
curl -X DELETE http://localhost:8000/trees/1
As you build your routes, remember these important principles:
1. Always validate input data before using it
2. Keep your route handlers focused on a single responsibility
3. Use clear, consistent naming for your routes and variables
4. Include helpful error messages that guide the API user
5. Consider implementing pagination for large data sets
6. Use transactions for operations that modify multiple records
To deepen your understanding, try:
1. Implementing search functionality
2. Adding pagination to your list routes
3. Creating more complex relationships between models
4. Adding validation hooks
5. Implementing soft deletes