Overview
Adoptme is built using a clean, layered architecture that follows the Repository Pattern with clear separation of concerns. The system is structured as an Express.js REST API with MongoDB as the data store, implementing a multi-tier architecture for maintainability and scalability.
Architecture Layers
The application follows a strict layered approach where each layer has a specific responsibility:
HTTP Request
↓
[Routes] - Define API endpoints
↓
[Controllers] - Handle request/response logic
↓
[Services] - Business logic orchestration
↓
[Repository] - Abstract data access layer
↓
[DAO] - Database operations
↓
[Models] - Mongoose schemas
↓
MongoDB
Application Entry Point
The Express application is initialized in src/app.js with the following configuration:
import express from 'express' ;
import mongoose from 'mongoose' ;
import cookieParser from 'cookie-parser' ;
import swaggerJSDoc from 'swagger-jsdoc' ;
import swaggerUI from 'swagger-ui-express' ;
const app = express ();
// MongoDB connection
const connection = mongoose . connect ( config . MONGO_URL , { dbName: config . DB_NAME })
. then (() => console . log ( "Connection successful MongoDB" ))
. catch (( err ) => {
console . error ({ error: err })
process . exit ( 1 )
})
// Middleware
app . use ( express . json ());
app . use ( cookieParser ());
// Route registration
app . use ( '/api/users' , usersRouter );
app . use ( '/api/pets' , petsRouter );
app . use ( '/api/adoptions' , adoptionsRouter );
app . use ( '/api/sessions' , sessionsRouter );
app . use ( '/api/mocks' , mockRouter );
app . use ( "/docs" , swaggerUI . serve , swaggerUI . setup ( spec ))
Key Components
Express.js - Web framework handling HTTP requests
Mongoose - ODM for MongoDB data modeling
Cookie Parser - Parses cookies for session management
Swagger - Auto-generated API documentation at /docs
1. Routes Layer Routes define the API endpoints and map them to controller methods. Example from src/routes/pets.router.js: import { Router } from 'express' ;
import petsController from '../controllers/pets.controller.js' ;
import uploader from '../utils/uploader.js' ;
const router = Router ();
router . get ( '/' , petsController . getAllPets );
router . post ( '/' , petsController . createPet );
router . post ( '/withimage' , uploader . single ( 'image' ), petsController . createPetWithImage );
router . put ( '/:pid' , petsController . updatePet );
router . delete ( '/:pid' , petsController . deletePet );
export default router ;
Responsibilities:
Define HTTP methods and paths
Apply middleware (e.g., uploader for file uploads)
Route requests to controllers
2. Controllers Layer Controllers handle HTTP request/response logic and coordinate with services. Example from src/controllers/pets.controller.js: import PetDTO from "../dto/Pet.dto.js" ;
import { petsService } from "../services/index.js"
const getAllPets = async ( req , res ) => {
try {
const pets = await petsService . getAll ();
res . send ({ status: "success" , payload: pets })
} catch ( error ) {
res . status ( 500 ). send ( "Ha ocurrido un error en la petición" )
}
}
const createPet = async ( req , res ) => {
try {
const { name , specie , birthDate } = req . body ;
if ( ! name || ! specie || ! birthDate ) {
return res . status ( 400 ). send ({ status: "error" , error: "Incomplete values" })
}
const pet = PetDTO . getPetInputFrom ({ name , specie , birthDate });
const result = await petsService . create ( pet );
res . send ({ status: "success" , payload: result })
} catch ( error ) {
res . status ( 500 ). send ( "Ha ocurrido un error en la petición" )
}
}
Responsibilities:
Validate request data
Transform data using DTOs
Call service layer methods
Format and send responses
Handle errors
3. Services Layer Services are instantiated with repository instances and provide business logic. From src/services/index.js: import Users from "../dao/Users.dao.js" ;
import Pet from "../dao/Pets.dao.js" ;
import Adoption from "../dao/Adoption.js" ;
import UserRepository from "../repository/UserRepository.js" ;
import PetRepository from "../repository/PetRepository.js" ;
import AdoptionRepository from "../repository/AdoptionRepository.js" ;
export const usersService = new UserRepository ( new Users ());
export const petsService = new PetRepository ( new Pet ());
export const adoptionsService = new AdoptionRepository ( new Adoption ());
Responsibilities:
Instantiate repositories with DAOs
Provide singleton service instances
Business logic orchestration
4. Repository Layer Repositories provide an abstraction over data access. The system uses a GenericRepository pattern from src/repository/GenericRepository.js: export default class GenericRepository {
constructor ( dao ) {
this . dao = dao ;
}
getAll = ( params ) => {
return this . dao . get ( params );
}
getBy = ( params ) => {
return this . dao . getBy ( params );
}
create = ( doc ) => {
return this . dao . save ( doc );
}
update = ( id , doc ) => {
return this . dao . update ( id , doc );
}
delete = ( id ) => {
return this . dao . delete ( id );
}
}
Specific repositories extend this base class. Example from src/repository/PetRepository.js: import GenericRepository from "./GenericRepository.js" ;
export default class PetRepository extends GenericRepository {
constructor ( dao ) {
super ( dao );
}
}
Responsibilities:
Abstract data access operations
Provide consistent interface for CRUD operations
Can be extended for domain-specific methods
5. DAO (Data Access Object) Layer DAOs directly interact with Mongoose models. Example from src/dao/Pets.dao.js: import petModel from "./models/Pet.js" ;
export default class Pet {
get = ( params ) => {
return petModel . find ( params )
}
getBy = ( params ) => {
return petModel . findOne ( params );
}
save = ( doc ) => {
return petModel . create ( doc );
}
update = ( id , doc ) => {
return petModel . findByIdAndUpdate ( id , { $set: doc })
}
delete = ( id ) => {
return petModel . findByIdAndDelete ( id );
}
}
Responsibilities:
Execute MongoDB operations via Mongoose
Translate repository calls to database queries
Return Mongoose documents
6. Models Layer Mongoose schemas define data structure. Example from src/dao/models/Pet.js: import mongoose from 'mongoose' ;
const collection = 'Pets' ;
const schema = new mongoose . Schema ({
name: {
type: String ,
required: true ,
},
specie: {
type: String ,
required: true
},
birthDate: Date ,
adopted: {
type: Boolean ,
default: false
},
owner: {
type: mongoose . SchemaTypes . ObjectId ,
ref: 'Users'
},
image: String
})
const petModel = mongoose . model ( collection , schema );
Responsibilities:
Define schema structure and validation
Set default values
Define relationships between collections
Create Mongoose model instances
The GenericRepository Pattern
The GenericRepository pattern is a key design decision in Adoptme that promotes code reusability and consistency.
The pattern provides several benefits:
Code Reusability - Common CRUD operations are defined once in GenericRepository
Consistency - All repositories share the same interface
Maintainability - Changes to data access logic are centralized
Testability - Easy to mock repositories for testing
Extensibility - Specific repositories can add custom methods
Implementation Flow
Here’s how a typical request flows through the layers:
// 1. Route receives request
GET / api / pets
// 2. Controller method is called
petsController . getAllPets ( req , res )
// 3. Service method is invoked
petsService . getAll () // PetRepository instance
// 4. Repository calls DAO
this . dao . get ( params ) // GenericRepository.getAll()
// 5. DAO queries MongoDB
petModel . find ( params ) // Pets.dao.get()
// 6. Results flow back up
DAO → Repository → Service → Controller → Response
Middleware & Utilities
The application uses several middleware components:
Cookie Parser Handles cookie parsing for JWT-based authentication: import cookieParser from 'cookie-parser' ;
app . use ( cookieParser ());
Multer File Upload Handles multipart/form-data for pet image uploads: import uploader from '../utils/uploader.js' ;
router . post ( '/withimage' , uploader . single ( 'image' ), petsController . createPetWithImage );
Password Hashing (bcrypt) Utility functions in src/utils/index.js for secure password handling: import bcrypt from 'bcrypt' ;
export const createHash = async ( password ) => {
const salts = await bcrypt . genSalt ( 10 );
return bcrypt . hash ( password , salts );
}
export const passwordValidation = async ( user , password ) => {
return bcrypt . compare ( password , user . password );
}
Data Transfer Objects (DTOs) transform data between layers, ensuring clean separation and security:
User.dto.js - Removes sensitive data (password) from JWT tokens
Pet.dto.js - Validates and sets defaults for pet creation
See Data Models for detailed DTO implementation.
API Documentation
The API is self-documented using Swagger/OpenAPI, configured in app.js:
const options = {
definition: {
openapi: "3.0.0" ,
info: {
title: "Adoptme" ,
version: "1.0.0" ,
description: "Documentación proyecto Adoptme"
}
},
apis: [ "./src/docs/*.yaml" ]
}
const spec = swaggerJSDoc ( options )
app . use ( "/docs" , swaggerUI . serve , swaggerUI . setup ( spec ))
Access interactive API documentation at /docs endpoint.
Technology Stack
Technology Version Purpose Express.js ^4.18.2 Web framework Mongoose ^6.7.5 MongoDB ODM bcrypt ^5.1.0 Password hashing jsonwebtoken ^8.5.1 JWT authentication cookie-parser ^1.4.6 Cookie handling multer ^1.4.5-lts.1 File uploads swagger-jsdoc ^6.2.8 API documentation swagger-ui-express ^5.0.1 Swagger UI
Best Practices
The architecture follows several best practices for Node.js/Express applications:
Separation of Concerns - Each layer has a single responsibility
Dependency Injection - DAOs are injected into repositories
Error Handling - Try-catch blocks in all async operations
Data Validation - Input validation at controller level
Security - Password hashing, JWT tokens, cookie-based sessions
Code Reusability - Generic repository pattern
ES6 Modules - Modern import/export syntax
Environment Configuration - Centralized config management
Next Steps