Skip to main content

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:
  1. Code Reusability - Common CRUD operations are defined once in GenericRepository
  2. Consistency - All repositories share the same interface
  3. Maintainability - Changes to data access logic are centralized
  4. Testability - Easy to mock repositories for testing
  5. 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
DAORepositoryServiceControllerResponse

Middleware & Utilities

The application uses several middleware components:
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 Transformation (DTOs)

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

TechnologyVersionPurpose
Express.js^4.18.2Web framework
Mongoose^6.7.5MongoDB ODM
bcrypt^5.1.0Password hashing
jsonwebtoken^8.5.1JWT authentication
cookie-parser^1.4.6Cookie handling
multer^1.4.5-lts.1File uploads
swagger-jsdoc^6.2.8API documentation
swagger-ui-express^5.0.1Swagger UI

Best Practices

The architecture follows several best practices for Node.js/Express applications:
  1. Separation of Concerns - Each layer has a single responsibility
  2. Dependency Injection - DAOs are injected into repositories
  3. Error Handling - Try-catch blocks in all async operations
  4. Data Validation - Input validation at controller level
  5. Security - Password hashing, JWT tokens, cookie-based sessions
  6. Code Reusability - Generic repository pattern
  7. ES6 Modules - Modern import/export syntax
  8. Environment Configuration - Centralized config management

Next Steps

Build docs developers (and LLMs) love