diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..baad9f2
--- /dev/null
+++ b/.dockerignore
@@ -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
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..3718fe1
--- /dev/null
+++ b/.env.example
@@ -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
diff --git a/.eslintrc b/.eslintrc
index 384fc3f..c2e8c12 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -12,10 +12,7 @@
],
"rules": {
"prettier/prettier": "error",
- "indent": [
- "error",
- 2
- ],
+ "indent": "off",
"no-unused-vars": "warn",
"no-console": "off"
}
diff --git a/.gitignore b/.gitignore
index 0a593f5..d5d5515 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
node_modules
.env
-
+*.log
+logs/
/interface-adapters/middlewares/logs/
/interface-adapters/controllers/examples
diff --git a/.husky/pre-push b/.husky/pre-push
index 0569d94..d0d7de5 100755
--- a/.husky/pre-push
+++ b/.husky/pre-push
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
-yarn lint && yarn format
+yarn lint && yarn format && yarn test
diff --git a/Dockerfile b/Dockerfile
index c89d6b9..2dd8d92 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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"]
\ No newline at end of file
+CMD ["node", "index.js"]
diff --git a/README.md b/README.md
index a1941b2..f880b9e 100644
--- a/README.md
+++ b/README.md
@@ -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
+

@@ -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
@@ -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
```
@@ -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
@@ -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
@@ -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
diff --git a/application-business-rules/use-cases/blogs/blog-handlers.js b/application-business-rules/use-cases/blogs/blog-handlers.js
index 07c6e4c..29915a7 100644
--- a/application-business-rules/use-cases/blogs/blog-handlers.js
+++ b/application-business-rules/use-cases/blogs/blog-handlers.js
@@ -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 });
@@ -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;
@@ -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 });
diff --git a/application-business-rules/use-cases/products/product-handlers.js b/application-business-rules/use-cases/products/product-handlers.js
index af3fd6e..5585555 100644
--- a/application-business-rules/use-cases/products/product-handlers.js
+++ b/application-business-rules/use-cases/products/product-handlers.js
@@ -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.
@@ -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,
@@ -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;
@@ -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,
@@ -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,
@@ -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);
}
};
diff --git a/application-business-rules/use-cases/user/index.js b/application-business-rules/use-cases/user/index.js
index 8b11900..524765a 100644
--- a/application-business-rules/use-cases/user/index.js
+++ b/application-business-rules/use-cases/user/index.js
@@ -1,92 +1,100 @@
-const userUseCases = require('./user-handlers');
+const authUseCases = require('./user-auth-usecases');
+const profileUseCases = require('./user-profile-usecases');
const { dbUserHandler } = require('../../../interface-adapters/database-access');
const { makeUser, validateId } = require('../../../enterprise-business-rules/entities');
const { RequiredParameterError } = require('../../../interface-adapters/validators-errors/errors');
-const { logEvents } = require('../../../interface-adapters/middlewares/loggers/logger');
+const { logEvents, log } = require('../../../interface-adapters/middlewares/loggers/logger');
const { makeHttpError } = require('../../../interface-adapters/validators-errors/http-error');
const entityModels = require('../../../enterprise-business-rules/entities');
-const registerUserUseCaseHandler = userUseCases.registerUserUseCase({
+// Auth Use Cases
+const registerUserUseCaseHandler = authUseCases.registerUserUseCase({
dbUserHandler,
entityModels,
logEvents,
+ log,
makeHttpError,
});
-
-const loginUserUseCaseHandler = userUseCases.loginUserUseCase({
+const loginUserUseCaseHandler = authUseCases.loginUserUseCase({
dbUserHandler,
logEvents,
+ log,
makeHttpError,
});
-
-const findOneUserUseCaseHandler = userUseCases.findOneUserUseCase({
+const logoutUseCaseHandler = authUseCases.logoutUseCase({ RequiredParameterError, logEvents, log });
+const refreshTokenUseCaseHandler = authUseCases.refreshTokenUseCase({
dbUserHandler,
- validateId,
+ RequiredParameterError,
logEvents,
+ log,
});
-
-const findAllUsersUseCaseHandler = userUseCases.findAllUsersUseCase({ dbUserHandler, logEvents });
-const logoutUseCaseHandler = userUseCases.logoutUseCase({ RequiredParameterError, logEvents });
-
-const refreshTokenUseCaseHandler = userUseCases.refreshTokenUseCase({
+const forgotPasswordUseCaseHandler = authUseCases.forgotPasswordUseCase({
dbUserHandler,
- RequiredParameterError,
logEvents,
+ log,
});
-
-const updateUserUseCaseHandler = userUseCases.updateUserUseCase({
+const resetPasswordUseCaseHandler = authUseCases.resetPasswordUseCase({
dbUserHandler,
- makeUser,
- validateId,
- RequiredParameterError,
logEvents,
+ log,
makeHttpError,
});
-const deleteUserUseCaseHandler = userUseCases.deleteUserUseCase({
+const findAllUsersUseCaseHandler = profileUseCases.findAllUsersUseCase({
+ dbUserHandler,
+ logEvents,
+});
+const findOneUserUseCaseHandler = profileUseCases.findOneUserUseCase({
dbUserHandler,
validateId,
- RequiredParameterError,
logEvents,
+ log,
});
-
-const blockUserUseCaseHandler = userUseCases.blockUserUseCase({
+const updateUserUseCaseHandler = profileUseCases.updateUserUseCase({
dbUserHandler,
+ makeUser,
validateId,
RequiredParameterError,
logEvents,
+ log,
+ makeHttpError,
});
-
-const unBlockUserUseCaseHandler = userUseCases.unBlockUserUseCase({
+const deleteUserUseCaseHandler = profileUseCases.deleteUserUseCase({
dbUserHandler,
validateId,
RequiredParameterError,
logEvents,
+ log,
});
-
-const forgotPasswordUseCaseHandler = userUseCases.forgotPasswordUseCase({
+const blockUserUseCaseHandler = profileUseCases.blockUserUseCase({
dbUserHandler,
+ validateId,
+ RequiredParameterError,
logEvents,
+ log,
});
-
-const resetPasswordUseCaseHandler = userUseCases.resetPasswordUseCase({
+const unBlockUserUseCaseHandler = profileUseCases.unBlockUserUseCase({
dbUserHandler,
+ validateId,
+ RequiredParameterError,
logEvents,
- makeHttpError,
+ log,
});
module.exports = {
+ // Auth
+ registerUserUseCaseHandler,
loginUserUseCaseHandler,
logoutUseCaseHandler,
refreshTokenUseCaseHandler,
- updateUserUseCaseHandler,
- deleteUserUseCaseHandler,
+ forgotPasswordUseCaseHandler,
+ resetPasswordUseCaseHandler,
+ // Profile
findAllUsersUseCaseHandler,
findOneUserUseCaseHandler,
- registerUserUseCaseHandler,
+ updateUserUseCaseHandler,
+ deleteUserUseCaseHandler,
blockUserUseCaseHandler,
unBlockUserUseCaseHandler,
- forgotPasswordUseCaseHandler,
- resetPasswordUseCaseHandler,
};
diff --git a/application-business-rules/use-cases/user/user-auth-usecases.js b/application-business-rules/use-cases/user/user-auth-usecases.js
new file mode 100644
index 0000000..f3714fc
--- /dev/null
+++ b/application-business-rules/use-cases/user/user-auth-usecases.js
@@ -0,0 +1,8 @@
+module.exports = {
+ registerUserUseCase: require('./user-handlers').registerUserUseCase,
+ loginUserUseCase: require('./user-handlers').loginUserUseCase,
+ refreshTokenUseCase: require('./user-handlers').refreshTokenUseCase,
+ logoutUseCase: require('./user-handlers').logoutUseCase,
+ forgotPasswordUseCase: require('./user-handlers').forgotPasswordUseCase,
+ resetPasswordUseCase: require('./user-handlers').resetPasswordUseCase,
+};
diff --git a/application-business-rules/use-cases/user/user-handlers.js b/application-business-rules/use-cases/user/user-handlers.js
index 0ad9c03..714d813 100644
--- a/application-business-rules/use-cases/user/user-handlers.js
+++ b/application-business-rules/use-cases/user/user-handlers.js
@@ -7,7 +7,7 @@ module.exports = {
* @return {Promise