🌿 Building a Complete Plant Registry System with Node.js, Express, and MongoDB
Introduction
After successfully implementing the animal registry system for MyZubster, the next logical step was to build a companion system for plants. This article documents how I built a complete, production-ready plant registry API with advanced search capabilities, statistics, and role-based access control.
MyZubster is an open-source project aiming to create a decentralized global map of plants and animals, powered by the Monero blockchain.
📌 Table of Contents
System Architecture
Data Model
API Endpoints
Controller Implementation
Advanced Features
Testing & Validation
Deployment
Next Steps
System Architecture
The system follows a clean MVC architecture:
text
src/
├── models/
│ ├── User.js
│ ├── Animal.js
│ └── Plant.js # New!
├── controllers/
│ ├── authController.js
│ ├── animalController.js
│ └── plantController.js # New!
├── routes/
│ ├── authRoutes.js
│ ├── animalRoutes.js
│ └── plantRoutes.js # New!
└── middleware/
└── auth.js
Tech Stack:
Node.js (v20+) with Express
MongoDB with Mongoose ODM
JWT for authentication
bcrypt for password hashing
PM2 for process management
Data Model
The plant schema captures detailed botanical information:
javascript
const PlantSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
scientificName: {
type: String,
trim: true
},
species: {
type: String,
required: true,
trim: true
},
family: {
type: String,
trim: true
},
genus: {
type: String,
trim: true
},
type: {
type: String,
enum: ['tree', 'shrub', 'herb', 'vine', 'succulent', 'aquatic', 'other'],
default: 'other'
},
height: {
type: Number,
min: 0
},
bloomSeason: {
type: String,
enum: ['spring', 'summer', 'autumn', 'winter', 'year-round'],
default: 'year-round'
},
registeredBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
location: {
lat: { type: Number },
lng: { type: Number },
address: { type: String },
city: { type: String },
country: { type: String }
},
verified: {
type: Boolean,
default: false
},
verifiedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
verifiedAt: {
type: Date
},
images: [{
url: String,
caption: String,
uploadedAt: { type: Date, default: Date.now }
}],
notes: {
type: String,
trim: true
},
conservationStatus: {
type: String,
enum: [
'least-concern',
'vulnerable',
'endangered',
'critically-endangered',
'extinct-in-wild',
'unknown'
],
default: 'unknown'
},
isEdible: {
type: Boolean,
default: false
},
isMedicinal: {
type: Boolean,
default: false
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
});
Performance Indexes
javascript
// Full-text search on name, scientific name, and species
PlantSchema.index({ name: 'text', scientificName: 'text', species: 'text' });
// Geolocation for map queries
PlantSchema.index({ location: '2dsphere' });
// Common query filters
PlantSchema.index({ species: 1 });
PlantSchema.index({ family: 1 });
PlantSchema.index({ verified: 1 });
PlantSchema.index({ createdAt: -1 });
API Endpoints
Method Endpoint Auth Description
GET /api/plants No List plants with filters and pagination
GET /api/plants/stats No Aggregated statistics
GET /api/plants/:id No Plant details
POST /api/plants/register JWT Register new plant
PUT /api/plants/:id JWT Update plant
DELETE /api/plants/:id JWT Delete plant
PATCH /api/plants/:id/verify Admin Verify plant
Controller Implementation
Plant Registration
javascript
exports.register = async (req, res) => {
try {
const { name, species, scientificName, family, genus, type } = req.body;
// Validation
if (!name || !species) {
return res.status(400).json({
success: false,
message: 'Name and species are required'
});
}
const plant = new Plant({
...req.body,
registeredBy: req.userId
});
await plant.save();
res.status(201).json({
success: true,
message: 'Plant registered successfully',
data: plant
});
} catch (error) {
console.error('Plant registration error:', error);
res.status(500).json({
success: false,
message: 'Error registering plant',
error: error.message
});
}
};
Advanced Search with Filters
javascript
exports.getAll = async (req, res) => {
try {
const {
species, family, type, verified,
search, limit = 50, page = 1
} = req.query;
const query = {};
// Build filter query
if (species) query.species = { $regex: species, $options: 'i' };
if (family) query.family = { $regex: family, $options: 'i' };
if (type) query.type = type;
if (verified !== undefined) query.verified = verified === 'true';
// Full-text search
if (search) {
query.$text = { $search: search };
}
const skip = (parseInt(page) - 1) * parseInt(limit);
const plants = await Plant.find(query)
.sort(search ? { score: { $meta: 'textScore' } } : { createdAt: -1 })
.skip(skip)
.limit(parseInt(limit))
.populate('registeredBy', 'username email')
.populate('verifiedBy', 'username email');
const total = await Plant.countDocuments(query);
res.json({
success: true,
count: plants.length,
total,
page: parseInt(page),
totalPages: Math.ceil(total / parseInt(limit)),
data: plants
});
} catch (error) {
console.error('Get plants error:', error);
res.status(500).json({
success: false,
message: 'Error retrieving plants',
error: error.message
});
}
};
Statistics & Aggregation
javascript
exports.getStats = async (req, res) => {
try {
const total = await Plant.countDocuments();
const verified = await Plant.countDocuments({ verified: true });
const species = await Plant.aggregate([
{ $group: { _id: '$species', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 10 }
]);
const types = await Plant.aggregate([
{ $group: { _id: '$type', count: { $sum: 1 } } }
]);
const conservationStatus = await Plant.aggregate([
{ $group: { _id: '$conservationStatus', count: { $sum: 1 } } }
]);
res.json({
success: true,
data: {
total,
verified,
unverified: total - verified,
topSpecies: species,
types,
conservationStatus
}
});
} catch (error) {
console.error('Get stats error:', error);
res.status(500).json({
success: false,
message: 'Error retrieving statistics',
error: error.message
});
}
};
Plant Verification (Admin Only)
javascript
exports.verify = async (req, res) => {
try {
if (req.userRole !== 'admin') {
return res.status(403).json({
success: false,
message: 'Admin privileges required'
});
}
const plant = await Plant.findById(req.params.id);
if (!plant) {
return res.status(404).json({
success: false,
message: 'Plant not found'
});
}
plant.verified = true;
plant.verifiedBy = req.userId;
plant.verifiedAt = new Date();
await plant.save();
res.json({
success: true,
message: 'Plant verified successfully',
data: plant
});
} catch (error) {
console.error('Verify plant error:', error);
res.status(500).json({
success: false,
message: 'Error verifying plant',
error: error.message
});
}
};
Advanced Features
- Full-Text Search
The system supports full-text search across name, scientific name, and species:
javascript
// Search query
const plants = await Plant.find({ $text: { $search: 'Quercia' } });
- Geospatial Queries
With 2dsphere indexes, the system can perform location-based queries:
javascript
// Find plants near a location
const plants = await Plant.find({
location: {
$near: {
$geometry: {
type: 'Point',
coordinates: [12.4964, 41.9028]
},
$maxDistance: 10000 // 10km
}
}
});
- Aggregation Pipelines
Complex statistics are generated using MongoDB aggregation:
javascript
const species = await Plant.aggregate([
{ $group: { _id: '$species', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 10 }
]);
- Role-Based Access Control javascript
// User middleware
exports.authenticate = async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.userId;
req.userRole = decoded.role;
next();
};
// Admin middleware
exports.isAdmin = (req, res, next) => {
if (req.userRole !== 'admin') {
return res.status(403).json({
success: false,
message: 'Admin privileges required'
});
}
next();
};
Testing & Validation
Test Results
bash
Register a plant
✅ Quercia registered successfully
✅ Rosmarino registered successfully
List plants
✅ 2 plants found
Filter by family
✅ Fagaceae filtered
Full-text search
✅ "Quercia" found
Statistics
✅ Total: 2
✅ Unverified: 2
✅ Top species: Quercus (1), Rosmarinus (1)
✅ Types: tree (1), shrub (1)
Update plant
✅ Height updated from 1.5 to 32
✅ Notes updated successfully
API Response Examples
Plant Registration Response:
json
{
"success": true,
"message": "Plant registered successfully",
"data": {
"_id": "6a6db509900129f91585b5a0",
"name": "Rosmarino",
"species": "Rosmarinus",
"family": "Lamiaceae",
"type": "shrub",
"height": 32,
"registeredBy": {
"_id": "6a6db349abdff6413b13ad0b",
"username": "danielioni",
"email": "daniel@example.com"
},
"verified": false,
"location": {
"lat": 41.903,
"lng": 12.496,
"city": "Roma",
"country": "Italia"
}
}
}
Statistics Response:
json
{
"success": true,
"data": {
"total": 2,
"verified": 0,
"unverified": 2,
"topSpecies": [
{ "_id": "Quercus", "count": 1 },
{ "_id": "Rosmarinus", "count": 1 }
],
"types": [
{ "_id": "tree", "count": 1 },
{ "_id": "shrub", "count": 1 }
],
"conservationStatus": [
{ "_id": "least-concern", "count": 2 }
]
}
}
Deployment
PM2 Process Management
bash
Start the gateway
pm2 start server.js --name myzubster-gateway
Monitor processes
pm2 status
View logs
pm2 logs myzubster-gateway
Auto-start on boot
pm2 save
pm2 startup
Environment Configuration
env
Server
PORT=10000
NODE_ENV=production
Database
MONGODB_URI=mongodb+srv://user:password@cluster.mongodb.net/db
JWT
JWT_SECRET=your-secret-key
JWT_EXPIRES_IN=7d
Feature Flags
ENABLE_PLANTS=true
ENABLE_ANIMALS=true
ENABLE_PAYMENTS=true
ENABLE_BOUNTY=true
Key Takeaways
What Worked Well
✅ MVC Architecture - Clean separation of concerns
✅ MongoDB Indexes - Fast queries even with large datasets
✅ JWT Auth - Secure and stateless
✅ Role-Based Access - Fine-grained permissions
✅ Aggregation Pipelines - Powerful statistics
✅ Full-Text Search - Flexible searching
Challenges Faced
⚠️ Index Management - Ensuring proper indexes for performance
⚠️ Validation - Complex validation rules for botanical data
⚠️ Pagination - Efficient pagination with large datasets
Solutions Implemented
🔧 2dsphere Indexes - For geospatial queries
🔧 Text Indexes - For full-text search
🔧 Aggregation Pipeline - For complex statistics
🔧 Populate - For relationship data
Next Steps
Monero Payment Integration
Implement payment gateway
Handle cryptocurrency transactions
Webhook support
Mobile App Integration
React Native frontend
Real-time updates
Offline support
Global Map
Visualize plants and animals
Interactive markers
Search and filter
Bounty System
Reward contributors
XMR payments
Automated payments
WebSocket Support
Real-time updates
Live notifications
Chat system
Project Links
GitHub: MyZubster Gateway
Mobile App: MyZubster App
Documentation: API Docs
Conclusion
The plant registry system is now fully functional and production-ready. Combined with the animal registry system, MyZubster now has a complete backend for managing botanical and zoological data on a global scale.
The system features:
✅ Complete CRUD operations
✅ Advanced search and filtering
✅ Statistics and aggregation
✅ Role-based access control
✅ Geospatial support
✅ Full-text search
✅ Image management
💚 Built with ❤️ for plants and animals by MyZubster-Ecosystem
Follow the project:
GitHub
Telegram
Twitter/X
Tags: #NodeJS #MongoDB #Express #API #JavaScript #OpenSource #PlantRegistry #MyZubster #MVC #RESTAPI
Full Code Repository
Check out the complete implementation:
bash
Top comments (0)