DEV Community

Daniel Ioni
Daniel Ioni

Posted on

🌿 Building a Complete Plant Registry System with Node.js, Express, and MongoDB

 🌿 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

} 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
});
Enter fullscreen mode Exit fullscreen mode

} 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
  }
});
Enter fullscreen mode Exit fullscreen mode

} 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
});
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('Verify plant error:', error);
res.status(500).json({
success: false,
message: 'Error verifying plant',
error: error.message
});
}
};

Advanced Features

  1. 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' } });

  1. 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
}
}
});

  1. 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 }
]);

  1. 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
Enter fullscreen mode Exit fullscreen mode

Project Links

GitHub: MyZubster Gateway

Mobile App: MyZubster App

Documentation: API Docs
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

💚 Built with ❤️ for plants and animals by MyZubster-Ecosystem

Follow the project:

GitHub

Telegram

Twitter/X
Enter fullscreen mode Exit fullscreen mode

Tags: #NodeJS #MongoDB #Express #API #JavaScript #OpenSource #PlantRegistry #MyZubster #MVC #RESTAPI
Full Code Repository

Check out the complete implementation:
bash

Top comments (0)