Skip to content

Commit f1f81e4

Browse files
authored
Merge pull request #7 from frckbrice/chore/update_dockerfile
Chore/update dockerfile
2 parents 6749a87 + 91d11b0 commit f1f81e4

55 files changed

Lines changed: 2511 additions & 1074 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Dependencies
2+
node_modules
3+
npm-debug.log*
4+
yarn-debug.log*
5+
yarn-error.log*
6+
7+
# Git and IDE
8+
.git
9+
.gitignore
10+
.github
11+
.husky
12+
.cursor
13+
.vscode
14+
.idea
15+
*.swp
16+
*.swo
17+
18+
# Tests and dev tooling
19+
tests
20+
coverage
21+
.nyc_output
22+
*.test.js
23+
*.spec.js
24+
jest.config.js
25+
.eslintrc
26+
.prettierrc
27+
.lintstagedrc.json
28+
29+
# Docs and local env (keep .env.example if app needs it at runtime; exclude if not)
30+
docs
31+
.env
32+
.env.local
33+
.env.*.local
34+
*.md
35+
!README.md
36+
37+
# Docker
38+
Dockerfile*
39+
docker-compose*
40+
.dockerignore
41+
42+
# Misc
43+
.DS_Store
44+
*.log

.env.example

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Server
2+
PORT=5000
3+
NODE_ENV=development
4+
5+
# MongoDB (use MONGO_URI or MONGODB_URI for rate-product transaction)
6+
MONGO_URI=mongodb://localhost:27017/your-db
7+
MONGO_DB_NAME=digital-market-place-updates
8+
9+
# JWT
10+
ACCESS_TOKEN_SECRETKEY=your_access_token_secret
11+
JWT_REFRESH_SECRET=your_refresh_token_secret
12+
JWT_EXPIRES_IN=15m
13+
JWT_REFRESH_EXPIRES_IN=7d
14+
15+
# Email (for password reset)
16+
MY_EMAIL=your-email@gmail.com
17+
PASSWORD=your-app-password
18+
19+
# CORS: add allowed origins in interface-adapters/middlewares/config/allowedOrigin.js

.eslintrc

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@
1212
],
1313
"rules": {
1414
"prettier/prettier": "error",
15-
"indent": [
16-
"error",
17-
2
18-
],
15+
"indent": "off",
1916
"no-unused-vars": "warn",
2017
"no-console": "off"
2118
}

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
node_modules
22
.env
3-
3+
*.log
4+
logs/
45
/interface-adapters/middlewares/logs/
56
/interface-adapters/controllers/examples

.husky/pre-push

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
#!/usr/bin/env sh
22
. "$(dirname -- "$0")/_/husky.sh"
33

4-
yarn lint && yarn format
4+
yarn lint && yarn format && yarn test

Dockerfile

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
1-
# Use official Node.js LTS image
2-
FROM node:18-alpine
3-
4-
# Set working directory
5-
WORKDIR /usr/src/app
6-
7-
# Copy package.json and yarn.lock
1+
# syntax=docker/dockerfile:1
2+
# ---- Dependencies stage ----
3+
FROM node:22-alpine AS deps
4+
WORKDIR /app
85
COPY package.json yarn.lock ./
6+
RUN yarn install --frozen-lockfile --production
97

10-
# Install dependencies
11-
RUN yarn install --production
8+
# ---- Production image ----
9+
FROM node:22-alpine AS runner
10+
WORKDIR /app
1211

13-
# Copy the rest of the application code
12+
# Create non-root user with fixed UID for consistency
13+
RUN addgroup -g 1001 -S appgroup && \
14+
adduser -S appuser -u 1001 -G appgroup
15+
16+
# Copy production dependencies from deps stage
17+
COPY --from=deps /app/node_modules ./node_modules
18+
COPY --from=deps /app/package.json ./package.json
1419
COPY . .
1520

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

19-
# Set environment variables
2024
ENV NODE_ENV=production
25+
EXPOSE 5000
26+
27+
USER appuser
28+
29+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
30+
CMD node -e "require('http').get('http://localhost:5000/', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"
2131

22-
# Start the app
23-
CMD ["yarn", "start"]
32+
CMD ["node", "index.js"]

README.md

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
# Clean code Architecture pattern applied to Node.js REST API Example
1+
2+
3+
# Clean code Architecture pattern applied to Node.js REST API Example
4+
25

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

912
> 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.
1013
14+
1115
## Stack
1216

1317
- **Node.js** (Express.js) for the REST API
@@ -35,6 +39,36 @@
3539
- 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.
3640
- Lower layers (domain, use cases) never import or reference Express, MongoDB, or any framework code.
3741

42+
=======
43+
44+
## Stack
45+
46+
- **Node.js** (Express.js) for the REST API
47+
- **MongoDB** (MongoClient) for persistence
48+
- **Jest** & **Supertest** for unit and integration testing
49+
- **ESLint** & **Prettier** for linting and formatting
50+
- **Docker** & **Docker Compose** for containerization
51+
- **GitHub Actions** for CI/CD
52+
53+
## Why Clean Architecture?
54+
55+
- **Separation of Concerns:** Each layer has a single responsibility and is independent from others.
56+
- **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.
57+
- **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.
58+
- **Security & Flexibility:** Infrastructure (DB, frameworks) can be swapped without touching business logic.
59+
60+
> **✨ Ultimate Flexibility:**
61+
> 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.
62+
63+
## How Testing Works
64+
65+
- **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.
66+
- **Integration tests** can use a real or in-memory database, but the architecture allows you to swap these easily.
67+
- **Example:**
68+
- 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.
69+
- Lower layers (domain, use cases) never import or reference Express, MongoDB, or any framework code.
70+
71+
3872
## Project Structure
3973

4074
```
@@ -52,6 +86,23 @@ routes/ # Express route definitions
5286
public/ # Static files and HTML views
5387
```
5488

89+
90+
## Features
91+
92+
- User registration and authentication (JWT)
93+
- Product CRUD operations
94+
- Blog and rating management
95+
- Role-based access control (admin, blocked users)
96+
- Input validation and error handling
97+
- Modular, testable codebase
98+
99+
## Stack
100+
- Express.js
101+
- Javascript
102+
- MongoDB doker image
103+
- Jest
104+
- Mongo-client + Mongosh
105+
55106
## Getting Started
56107

57108
### Prerequisites
@@ -70,12 +121,7 @@ public/ # Static files and HTML views
70121
```bash
71122
yarn install
72123
```
73-
3. Create a `.env` file in the root with your environment variables:
74-
```env
75-
PORT=5000
76-
MONGO_URI=mongodb://localhost:27017/your-db
77-
JWT_SECRET=your_jwt_secret
78-
```
124+
3. Copy `.env.example` to `.env` and set your environment variables. For production, set `NODE_ENV=production`
79125
4. Start the server:
80126
```bash
81127
yarn dev
@@ -147,7 +193,7 @@ See the `routes/` directory for all endpoints. Example:
147193

148194
## Troubleshooting
149195

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

152198
## License
153199

application-business-rules/use-cases/blogs/blog-handlers.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Blog use cases (Clean Architecture)
22
module.exports = {
3-
createBlogUseCase: ({ dbBlogHandler, makeBlogModel, logEvents, errorHandlers }) =>
3+
createBlogUseCase: ({ dbBlogHandler, makeBlogModel, logEvents }) =>
44
async function createBlogUseCaseHandler(blogData) {
55
try {
66
const validatedBlog = await makeBlogModel({ blogData });
@@ -16,7 +16,7 @@ module.exports = {
1616
async function findAllBlogsUseCaseHandler() {
1717
try {
1818
const blogs = await dbBlogHandler.findAllBlogs();
19-
return blogs || [];
19+
return Object.freeze(blogs.flat().data);
2020
} catch (error) {
2121
logEvents && logEvents(error.message, 'blogUseCase.log');
2222
throw error;
@@ -35,7 +35,7 @@ module.exports = {
3535
}
3636
},
3737

38-
updateBlogUseCase: ({ dbBlogHandler, makeBlogModel, logEvents, errorHandlers }) =>
38+
updateBlogUseCase: ({ dbBlogHandler, makeBlogModel, logEvents }) =>
3939
async function updateBlogUseCaseHandler({ blogId, updateData }) {
4040
try {
4141
const existingBlog = await dbBlogHandler.findOneBlog({ blogId });

application-business-rules/use-cases/products/product-handlers.js

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
'use strict';
2+
13
const productValidationFcts = require('../../../enterprise-business-rules/validate-models/product-validation-fcts');
2-
// const { findAllProductUseCaseHandler } = require('./product-handlers');
4+
const { log } = require('../../../interface-adapters/middlewares/loggers/logger');
35

46
/**
57
* Creates a new product in the database using the provided product data.
@@ -24,12 +26,14 @@ const createProductUseCase = ({ makeProductModelHandler }) =>
2426
const newProduct = await createProductDbHandler(validatedProductData);
2527
return Object.freeze(newProduct);
2628
} catch (error) {
27-
console.log('Error from create product handler: ', error);
29+
log.error('Error from create product handler:', error.message);
2830
throw new Error(error.message);
2931
}
3032
};
3133

32-
//find one product from DB
34+
/**
35+
* Fetches a single product by ID.
36+
*/
3337
const findOneProductUseCase = ({ productValidation }) =>
3438
async function findOneProductUseCaseHandler({
3539
productId,
@@ -44,25 +48,28 @@ const findOneProductUseCase = ({ productValidation }) =>
4448
const newProduct = await findOneProductDbHandler({ productId: uuid });
4549
return Object.freeze(newProduct);
4650
} catch (error) {
47-
console.log('Error from fetch one product handler: ', error);
51+
log.error('Error from fetch one product handler:', error.message);
4852
throw new Error(error.message);
4953
}
5054
};
5155

52-
// find all product use case handler
56+
/**
57+
* Fetches all products with optional filters.
58+
*/
5359
const findAllProductsUseCase = () =>
5460
async function findAllProductUseCaseHandler({ dbProductHandler, filterOptions }) {
5561
try {
5662
const allProducts = await dbProductHandler.findAllProductsDbHandler(filterOptions);
57-
// console.log("from find all products use case: ", allProducts);
58-
return Object.freeze(allProducts);
63+
return Object.freeze(allProducts.data);
5964
} catch (e) {
60-
console.log('Error from fetch all product handler: ', e);
65+
log.error('Error from fetch all product handler:', e.message);
6166
throw new Error(e.message);
6267
}
6368
};
6469

65-
// delete product use case
70+
/**
71+
* Deletes a product by ID.
72+
*/
6673
const deleteProductUseCase = () =>
6774
async function deleteProductUseCaseHandler({ productId, dbProductHandler, errorHandlers }) {
6875
const { findOneProductDbHandler, deleteProductDbHandler } = dbProductHandler;
@@ -83,12 +90,14 @@ const deleteProductUseCase = () =>
8390
};
8491
return Object.freeze(result);
8592
} catch (error) {
86-
console.log('Error from delete product handler: ', error);
93+
log.error('Error from delete product handler:', error.message);
8794
throw new Error(error.message);
8895
}
8996
};
9097

91-
// update product
98+
/**
99+
* Updates a product by ID.
100+
*/
92101
const updateProductUseCase = ({ makeProductModelHandler }) =>
93102
async function updateProductUseCaseHandler({
94103
productId,
@@ -113,17 +122,17 @@ const updateProductUseCase = ({ makeProductModelHandler }) =>
113122
errorHandlers,
114123
});
115124

116-
// store product in database mongodb
117125
const newProduct = await updateProductDbHandler({ productId, ...productData });
118-
console.log(' from product handler after DB: ', newProduct);
119126
return Object.freeze(newProduct);
120127
} catch (error) {
121-
console.log('Error from update product handler: ', error);
128+
log.error('Error from update product handler:', error.message);
122129
throw new Error(error.message);
123130
}
124131
};
125132

126-
// rate product in transaction with both Rate model and Product model
133+
/**
134+
* Rates a product (creates rating and updates product aggregates in a transaction).
135+
*/
127136
const rateProductUseCase = ({ makeProductRatingModelHandler }) =>
128137
async function rateProductUseCaseHandler({
129138
userId,
@@ -132,16 +141,13 @@ const rateProductUseCase = ({ makeProductRatingModelHandler }) =>
132141
dbProductHandler,
133142
errorHandlers,
134143
}) {
135-
console.log('hit rating use case handler');
136144
const ratingData = { ratingValue, userId, productId };
137145
try {
138-
/* validate and build rating model */
139146
const ratingModel = await makeProductRatingModelHandler({ errorHandlers, ...ratingData });
140147
const newProduct = await dbProductHandler.rateProductDbHandler(ratingModel);
141-
console.log(' from rating product handler after DB: ', newProduct);
142148
return Object.freeze(newProduct);
143149
} catch (error) {
144-
console.log('Error from fetch one product handler: ', error);
150+
log.error('Error from rating product handler:', error.message);
145151
throw new Error(error.message);
146152
}
147153
};

0 commit comments

Comments
 (0)