🐾 Building a Complete Animal Registration System with Node.js
and MongoDB
Introduction
In this article, I'll walk you through how I built a complete animal registration and management system for MyZubster - a project that aims to create a decentralized global map of plants and animals, powered by the Monero blockchain.
📌 Table of Contents
System Architecture
Data Model
API Endpoints
Authentication & Authorization
Controller Implementation
Testing & Validation
Next Steps
System Architecture
The system follows an MVC (Model-View-Controller) architecture using:
Node.js + Express for the backend
MongoDB + Mongoose for the database
JWT for authentication
bcrypt for password hashing
text
src/
├── models/
│ └── Animal.js # Data model
├── controllers/
│ └── animalController.js # Business logic
├── routes/
│ └── animalRoutes.js # Endpoint definitions
└── middleware/
└── auth.js # JWT authentication
Data Model
Here's the complete model for animal registration:
javascript
const AnimalSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
species: {
type: String,
required: true,
trim: true
},
breed: {
type: String,
trim: true
},
age: {
type: Number,
min: 0,
max: 100
},
gender: {
type: String,
enum: ['male', 'female', 'other'],
default: 'other'
},
color: {
type: String,
trim: true
},
microchipNumber: {
type: String,
trim: true,
unique: true,
sparse: true
},
owner: {
type: String,
required: true,
trim: true
},
ownerEmail: {
type: String,
trim: true,
lowercase: true
},
ownerPhone: {
type: String,
trim: true
},
registeredBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
location: {
lat: { type: Number },
lng: { type: Number },
address: { type: String, trim: true },
city: { type: String, trim: true },
country: { type: String, trim: true }
},
verified: {
type: Boolean,
default: false
},
verifiedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
verifiedAt: {
type: Date
},
imageUrl: {
type: String
},
images: [{
url: String,
caption: String,
uploadedAt: { type: Date, default: Date.now }
}],
notes: {
type: String,
trim: true
},
healthStatus: {
type: String,
enum: ['healthy', 'treatment', 'critical', 'unknown'],
default: 'unknown'
},
vaccinations: [{
name: String,
date: Date,
nextDue: Date,
notes: String
}],
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
});
Performance Indexes:
javascript
AnimalSchema.index({ name: 'text', species: 'text', breed: 'text', owner: 'text' });
AnimalSchema.index({ location: '2dsphere' });
AnimalSchema.index({ species: 1 });
AnimalSchema.index({ owner: 1 });
AnimalSchema.index({ verified: 1 });
AnimalSchema.index({ createdAt: -1 });
API Endpoints
Endpoint Method Auth Description
/api/animals GET No List animals with filters & pagination
/api/animals/stats GET No Animal statistics
/api/animals/:id GET No Animal details
/api/animals/register POST JWT Register a new animal
/api/animals/:id PUT JWT Update animal
/api/animals/:id DELETE JWT Delete animal
/api/animals/:id/verify PATCH Admin Verify animal
Authentication & Authorization
javascript
// JWT Authentication Middleware
exports.authenticate = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: 'Authentication token required'
});
}
const token = authHeader.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.userId;
req.userRole = decoded.role;
req.username = decoded.username;
next();
} catch (error) {
res.status(401).json({
success: false,
message: 'Invalid or expired token'
});
}
};
// Admin Authorization Middleware
exports.isAdmin = (req, res, next) => {
if (req.userRole !== 'admin') {
return res.status(403).json({
success: false,
message: 'Admin privileges required'
});
}
next();
};
Controller Implementation
Animal Registration
javascript
exports.register = async (req, res) => {
try {
const {
name, species, breed, age, gender, color,
microchipNumber, owner, ownerEmail, ownerPhone,
location, imageUrl, notes, healthStatus
} = req.body;
// Validation
if (!name || !species || !owner) {
return res.status(400).json({
success: false,
message: 'Name, species, and owner are required'
});
}
// Check for duplicate microchip
if (microchipNumber) {
const existing = await Animal.findOne({ microchipNumber });
if (existing) {
return res.status(400).json({
success: false,
message: 'Microchip already registered'
});
}
}
const animal = new Animal({
name,
species,
breed,
age,
gender,
color,
microchipNumber,
owner,
ownerEmail,
ownerPhone,
location,
imageUrl,
notes,
healthStatus,
registeredBy: req.userId
});
await animal.save();
res.status(201).json({
success: true,
message: 'Animal registered successfully',
data: animal
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({
success: false,
message: 'Error registering animal',
error: error.message
});
}
};
Search with Filters
javascript
exports.getAll = async (req, res) => {
try {
const {
species, breed, owner, verified,
search, limit = 50, page = 1
} = req.query;
const query = {};
if (species) query.species = species;
if (breed) query.breed = breed;
if (owner) query.owner = { $regex: owner, $options: 'i' };
if (verified !== undefined) query.verified = verified === 'true';
// Full-text search
if (search) {
query.$text = { $search: search };
}
const skip = (parseInt(page) - 1) * parseInt(limit);
const animals = await Animal.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 Animal.countDocuments(query);
res.json({
success: true,
count: animals.length,
total,
page: parseInt(page),
totalPages: Math.ceil(total / parseInt(limit)),
data: animals
});
} catch (error) {
console.error('Get animals error:', error);
res.status(500).json({
success: false,
message: 'Error retrieving animals',
error: error.message
});
}
};
Statistics
javascript
exports.getStats = async (req, res) => {
try {
const total = await Animal.countDocuments();
const verified = await Animal.countDocuments({ verified: true });
const species = await Animal.aggregate([
{ $group: { _id: '$species', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 10 }
]);
res.json({
success: true,
data: {
total,
verified,
unverified: total - verified,
topSpecies: species
}
});
} catch (error) {
console.error('Stats error:', error);
res.status(500).json({
success: false,
message: 'Error retrieving statistics'
});
}
};
Testing
Register an Animal
bash
First, login to get a JWT token
TOKEN=$(curl -s -X POST http://localhost:10000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"password123"}' \
| jq -r '.data.token')
Register an animal
curl -X POST http://localhost:10000/api/animals/register \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Fido",
"species": "Dog",
"breed": "Golden Retriever",
"age": 3,
"gender": "male",
"color": "Golden",
"owner": "John Doe",
"ownerEmail": "john@example.com",
"location": {
"lat": 41.9028,
"lng": 12.4964,
"city": "Rome",
"country": "Italy"
}
}' | jq '.'
List Animals with Filters
bash
Get all animals
curl "http://localhost:10000/api/animals" | jq '.'
Search by species
curl "http://localhost:10000/api/animals?species=Dog" | jq '.'
Full-text search
curl "http://localhost:10000/api/animals?search=Fido" | jq '.'
Get statistics
curl "http://localhost:10000/api/animals/stats" | jq '.'
Animal Verification (Admin Only)
bash
Verify an animal (admin required)
curl -X PATCH http://localhost:10000/api/animals/{animalId}/verify \
-H "Authorization: Bearer $ADMIN_TOKEN" | jq '.'
Key Features Implemented
✅ Full CRUD Operations - Create, Read, Update, Delete
✅ JWT Authentication - Secure API endpoints
✅ Role-Based Access - User vs Admin permissions
✅ Advanced Search - Full-text search with filters
✅ Geolocation - Location data with MongoDB 2dsphere indexes
✅ Pagination - Efficient data loading
✅ Statistics - Aggregated animal data
✅ Microchip Tracking - Unique identification
✅ Verification System - Admin approval workflow
✅ Image Support - Multiple images per animal
✅ Health Status - Track animal health conditions
✅ Vaccination Records - Track vaccinations and due dates
Next Steps
Plant Registration - Build similar system for plants
Monero Payments - Integrate cryptocurrency payments
Frontend Integration - React Native mobile app
Real-time Updates - WebSocket for live data
Mobile App - Build the Expo/React Native app
Monero Wallet - Integrate payment gateway
Global Map - Visualize animals on an interactive map
Tech Stack Summary
Technology Purpose
Node.js Backend runtime
Express Web framework
MongoDB Database
Mongoose ODM for MongoDB
JWT Authentication
bcrypt Password hashing
PM2 Process management
Repository
Check out the full source code on GitHub:
Project: MyZubster Gateway
App: MyZubster Mobile App
Conclusion
Building this animal registration system was a great experience in creating a robust, secure, and scalable API. The combination of Node.js, Express, and MongoDB with proper authentication and authorization patterns makes it production-ready.
The system is now fully functional and ready for the next phase - integrating the React Native mobile app and the Monero payment gateway.
💚 Built with ❤️ for animals and plants by MyZubster-Ecosystem
Follow the project:
GitHub
Telegram
Twitter/X
Tag: #NodeJS #
#JavaScript #OpenSource #AnimalRegistry #MyZubster
Top comments (0)