Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Git and IDE
.git
.gitignore
.github
.husky
.cursor
.vscode
.idea
*.swp
*.swo

# Tests and dev tooling
tests
coverage
.nyc_output
*.test.js
*.spec.js
jest.config.js
.eslintrc
.prettierrc
.lintstagedrc.json

# Docs and local env (keep .env.example if app needs it at runtime; exclude if not)
docs
.env
.env.local
.env.*.local
*.md
!README.md

# Docker
Dockerfile*
docker-compose*
.dockerignore

# Misc
.DS_Store
*.log
19 changes: 19 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Server
PORT=5000
NODE_ENV=development

# MongoDB (use MONGO_URI or MONGODB_URI for rate-product transaction)
MONGO_URI=mongodb://localhost:27017/your-db
MONGO_DB_NAME=digital-market-place-updates

# JWT
ACCESS_TOKEN_SECRETKEY=your_access_token_secret
JWT_REFRESH_SECRET=your_refresh_token_secret
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d

# Email (for password reset)
MY_EMAIL=your-email@gmail.com
PASSWORD=your-app-password

# CORS: add allowed origins in interface-adapters/middlewares/config/allowedOrigin.js
5 changes: 1 addition & 4 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@
],
"rules": {
"prettier/prettier": "error",
"indent": [
"error",
2
],
"indent": "off",
"no-unused-vars": "warn",
"no-console": "off"
}
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules
.env

*.log
logs/
/interface-adapters/middlewares/logs/
/interface-adapters/controllers/examples
2 changes: 1 addition & 1 deletion .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

yarn lint && yarn format
yarn lint && yarn format && yarn test
39 changes: 24 additions & 15 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
# Use official Node.js LTS image
FROM node:18-alpine

# Set working directory
WORKDIR /usr/src/app

# Copy package.json and yarn.lock
# syntax=docker/dockerfile:1
# ---- Dependencies stage ----
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile --production

# Install dependencies
RUN yarn install --production
# ---- Production image ----
FROM node:22-alpine AS runner
WORKDIR /app

# Copy the rest of the application code
# Create non-root user with fixed UID for consistency
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup

# Copy production dependencies from deps stage
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/package.json ./package.json
COPY . .

# Expose the port the app runs on
EXPOSE 5000
# Ensure app files are readable by appuser (write not required at runtime)
RUN chown -R appuser:appgroup /app

# Set environment variables
ENV NODE_ENV=production
EXPOSE 5000

USER appuser

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:5000/', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"

# Start the app
CMD ["yarn", "start"]
CMD ["node", "index.js"]
62 changes: 54 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Clean code Architecture pattern applied to Node.js REST API Example


# Clean code Architecture pattern applied to Node.js REST API Example


<div style="width:100%; text-align:center">
<img src="public/images/clean-code_arch.jpeg" width="600">
Expand All @@ -8,6 +11,7 @@

> This project demonstrates how to apply Uncle Bob's Clean Architecture principles in a Node.js REST API. It is designed as an educational resource to help developers structure their projects for maximum testability, maintainability, and scalability. The codebase shows how to keep business logic independent from frameworks, databases, and delivery mechanisms.


## Stack

- **Node.js** (Express.js) for the REST API
Expand Down Expand Up @@ -35,6 +39,36 @@
- The product use case receives a `createProductDbHandler` as a parameter. In production, this is the real DB handler; in tests, it's a mock function.
- Lower layers (domain, use cases) never import or reference Express, MongoDB, or any framework code.

=======

## Stack

- **Node.js** (Express.js) for the REST API
- **MongoDB** (MongoClient) for persistence
- **Jest** & **Supertest** for unit and integration testing
- **ESLint** & **Prettier** for linting and formatting
- **Docker** & **Docker Compose** for containerization
- **GitHub Actions** for CI/CD

## Why Clean Architecture?

- **Separation of Concerns:** Each layer has a single responsibility and is independent from others.
- **Dependency Rule:** Data and control flow from outer layers (e.g., routes/controllers) to inner layers (use cases, domain), never the reverse. Lower layers are unaware of upper layers.
- **Testability:** Business logic can be tested in isolation by injecting dependencies (e.g., mock DB handlers) from above. No real database is needed for unit tests.
- **Security & Flexibility:** Infrastructure (DB, frameworks) can be swapped without touching business logic.

> **✨ Ultimate Flexibility:**
> This project demonstrates that your core business logic is never tied to any specific framework, ORM, or database. You can switch from Express to Fastify, MongoDB to PostgreSQL, or even move to a serverless environment—without rewriting your business rules. The architecture ensures your codebase adapts easily to new technologies, making future migrations and upgrades painless. This is true Clean Architecture in action: your app’s heart beats independently of any tool or vendor.

## How Testing Works

- **Unit tests** inject mocks for all dependencies (DB, loggers, etc.) into use cases and controllers. This means you can test all business logic without a real database or server.
- **Integration tests** can use a real or in-memory database, but the architecture allows you to swap these easily.
- **Example:**
- The product use case receives a `createProductDbHandler` as a parameter. In production, this is the real DB handler; in tests, it's a mock function.
- Lower layers (domain, use cases) never import or reference Express, MongoDB, or any framework code.


## Project Structure

```
Expand All @@ -52,6 +86,23 @@ routes/ # Express route definitions
public/ # Static files and HTML views
```


## Features

- User registration and authentication (JWT)
- Product CRUD operations
- Blog and rating management
- Role-based access control (admin, blocked users)
- Input validation and error handling
- Modular, testable codebase

## Stack
- Express.js
- Javascript
- MongoDB doker image
- Jest
- Mongo-client + Mongosh

## Getting Started

### Prerequisites
Expand All @@ -70,12 +121,7 @@ public/ # Static files and HTML views
```bash
yarn install
```
3. Create a `.env` file in the root with your environment variables:
```env
PORT=5000
MONGO_URI=mongodb://localhost:27017/your-db
JWT_SECRET=your_jwt_secret
```
3. Copy `.env.example` to `.env` and set your environment variables. For production, set `NODE_ENV=production`
4. Start the server:
```bash
yarn dev
Expand Down Expand Up @@ -147,7 +193,7 @@ See the `routes/` directory for all endpoints. Example:

## Troubleshooting

- See [troubleshooting.md](./troubleshooting.md) for common issues and solutions.
- See [troubleshooting.md](./docs/troubleshooting.md) for common issues and solutions.

## License

Expand Down
6 changes: 3 additions & 3 deletions application-business-rules/use-cases/blogs/blog-handlers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Blog use cases (Clean Architecture)
module.exports = {
createBlogUseCase: ({ dbBlogHandler, makeBlogModel, logEvents, errorHandlers }) =>
createBlogUseCase: ({ dbBlogHandler, makeBlogModel, logEvents }) =>
async function createBlogUseCaseHandler(blogData) {
try {
const validatedBlog = await makeBlogModel({ blogData });
Expand All @@ -16,7 +16,7 @@ module.exports = {
async function findAllBlogsUseCaseHandler() {
try {
const blogs = await dbBlogHandler.findAllBlogs();
return blogs || [];
return Object.freeze(blogs.flat().data);
} catch (error) {
logEvents && logEvents(error.message, 'blogUseCase.log');
throw error;
Expand All @@ -35,7 +35,7 @@ module.exports = {
}
},

updateBlogUseCase: ({ dbBlogHandler, makeBlogModel, logEvents, errorHandlers }) =>
updateBlogUseCase: ({ dbBlogHandler, makeBlogModel, logEvents }) =>
async function updateBlogUseCaseHandler({ blogId, updateData }) {
try {
const existingBlog = await dbBlogHandler.findOneBlog({ blogId });
Expand Down
44 changes: 25 additions & 19 deletions application-business-rules/use-cases/products/product-handlers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

const productValidationFcts = require('../../../enterprise-business-rules/validate-models/product-validation-fcts');
// const { findAllProductUseCaseHandler } = require('./product-handlers');
const { log } = require('../../../interface-adapters/middlewares/loggers/logger');

/**
* Creates a new product in the database using the provided product data.
Expand All @@ -24,12 +26,14 @@ const createProductUseCase = ({ makeProductModelHandler }) =>
const newProduct = await createProductDbHandler(validatedProductData);
return Object.freeze(newProduct);
} catch (error) {
console.log('Error from create product handler: ', error);
log.error('Error from create product handler:', error.message);
throw new Error(error.message);
}
};

//find one product from DB
/**
* Fetches a single product by ID.
*/
const findOneProductUseCase = ({ productValidation }) =>
async function findOneProductUseCaseHandler({
productId,
Expand All @@ -44,25 +48,28 @@ const findOneProductUseCase = ({ productValidation }) =>
const newProduct = await findOneProductDbHandler({ productId: uuid });
return Object.freeze(newProduct);
} catch (error) {
console.log('Error from fetch one product handler: ', error);
log.error('Error from fetch one product handler:', error.message);
throw new Error(error.message);
}
};

// find all product use case handler
/**
* Fetches all products with optional filters.
*/
const findAllProductsUseCase = () =>
async function findAllProductUseCaseHandler({ dbProductHandler, filterOptions }) {
try {
const allProducts = await dbProductHandler.findAllProductsDbHandler(filterOptions);
// console.log("from find all products use case: ", allProducts);
return Object.freeze(allProducts);
return Object.freeze(allProducts.data);
} catch (e) {
console.log('Error from fetch all product handler: ', e);
log.error('Error from fetch all product handler:', e.message);
throw new Error(e.message);
}
};

// delete product use case
/**
* Deletes a product by ID.
*/
const deleteProductUseCase = () =>
async function deleteProductUseCaseHandler({ productId, dbProductHandler, errorHandlers }) {
const { findOneProductDbHandler, deleteProductDbHandler } = dbProductHandler;
Expand All @@ -83,12 +90,14 @@ const deleteProductUseCase = () =>
};
return Object.freeze(result);
} catch (error) {
console.log('Error from delete product handler: ', error);
log.error('Error from delete product handler:', error.message);
throw new Error(error.message);
}
};

// update product
/**
* Updates a product by ID.
*/
const updateProductUseCase = ({ makeProductModelHandler }) =>
async function updateProductUseCaseHandler({
productId,
Expand All @@ -113,17 +122,17 @@ const updateProductUseCase = ({ makeProductModelHandler }) =>
errorHandlers,
});

// store product in database mongodb
const newProduct = await updateProductDbHandler({ productId, ...productData });
console.log(' from product handler after DB: ', newProduct);
return Object.freeze(newProduct);
} catch (error) {
console.log('Error from update product handler: ', error);
log.error('Error from update product handler:', error.message);
throw new Error(error.message);
}
};

// rate product in transaction with both Rate model and Product model
/**
* Rates a product (creates rating and updates product aggregates in a transaction).
*/
const rateProductUseCase = ({ makeProductRatingModelHandler }) =>
async function rateProductUseCaseHandler({
userId,
Expand All @@ -132,16 +141,13 @@ const rateProductUseCase = ({ makeProductRatingModelHandler }) =>
dbProductHandler,
errorHandlers,
}) {
console.log('hit rating use case handler');
const ratingData = { ratingValue, userId, productId };
try {
/* validate and build rating model */
const ratingModel = await makeProductRatingModelHandler({ errorHandlers, ...ratingData });
const newProduct = await dbProductHandler.rateProductDbHandler(ratingModel);
console.log(' from rating product handler after DB: ', newProduct);
return Object.freeze(newProduct);
} catch (error) {
console.log('Error from fetch one product handler: ', error);
log.error('Error from rating product handler:', error.message);
throw new Error(error.message);
}
};
Expand Down
Loading
Loading