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
3 changes: 3 additions & 0 deletions .commitlintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
};
13 changes: 13 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
root = true

[*.{ts,js,json,yml,yaml,md,scss,html}]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

66 changes: 66 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: CI

on:
push:
branches:
- '**'
pull_request:

jobs:
backend:
name: Backend Lint/Test/Build
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: backend/package-lock.json

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Test
run: npm run test

- name: Build
run: npm run build

frontend:
name: Frontend Lint/Test/Build
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Test
run: npm run test

- name: Build
run: npm run build
30 changes: 30 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# dependencies
node_modules/

# logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# environment files
.env
.env.*
!.env.example

# builds and coverage
dist/
coverage/
tmp/
.tmp/
.cache/

# IDE/OS
.DS_Store
.idea/
.vscode/

# Docker
*.pid

1 change: 1 addition & 0 deletions .husky/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx --no -- commitlint --edit "$1"
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npm run lint-staged
5 changes: 5 additions & 0 deletions .lintstagedrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
'*.{js,ts,mjs,cjs,json,md,scss,html,yml,yaml}': ['prettier --write'],
'backend/**/*.{ts,js}': ['npm exec --prefix backend eslint --fix --max-warnings=0'],
'frontend/**/*.{ts,js}': ['npm exec --prefix frontend eslint --fix --max-warnings=0'],
};
9 changes: 9 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
dist
coverage
frontend/.angular
frontend/dist
backend/dist
package-lock.json
frontend/package-lock.json
backend/package-lock.json
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"singleQuote": true,
"semi": true,
"printWidth": 100,
"trailingComma": "all"
}
82 changes: 81 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,81 @@
# mean-boilerplate
# Cloud-Native MEAN Boilerplate (TypeScript, 12-Factor Ready)

Production-oriented MEAN stack starter with:

- **MongoDB + Mongoose**
- **Express + Node.js 20+**
- **Angular 17+ style architecture (standalone, lazy routes, signals, control flow)**
- **TypeScript strict mode** on frontend and backend
- **Docker multi-stage images** for backend and frontend (Nginx)
- **docker-compose** for local full-stack development
- **CI pipeline** (GitHub Actions) for lint, test, and build
- **Git hooks** (`husky` + `lint-staged`) and **Conventional Commit** enforcement (`commitlint`)

## Repository Structure

```text
.
├── backend
├── frontend
├── docker-compose.yml
└── .github/workflows/ci.yml
```

## Quick Start

### 1) Backend

```bash
cd backend
cp .env.example .env
npm install
npm run dev
```

### 2) Frontend

```bash
cd frontend
npm install
npm start
```

### 3) Full stack with Docker

```bash
docker compose up --build
```

- Frontend: `http://localhost:4200`
- Backend API: `http://localhost:3000`
- MongoDB: `mongodb://localhost:27017`

## API Endpoints (core)

- `GET /health/liveness`
- `GET /health/readiness`
- `POST /api/v1/auth/login`
- `GET /api/v1/users`
- `POST /api/v1/users`

All API responses follow:

```json
{
"success": true,
"data": {}
}
```

or

```json
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Readable message",
"details": {}
}
}
```
7 changes: 7 additions & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
dist
coverage
npm-debug.log
.env
.env.*
!.env.example
16 changes: 16 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# App
NODE_ENV=development
PORT=3000
API_PREFIX=/api/v1
LOG_LEVEL=info

# Database
MONGODB_URI=mongodb://localhost:27017/mean_app

# Security
CORS_ORIGIN=http://localhost:4200
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100
# Required in production; do not use weak/shared values.
JWT_SECRET=replace-with-a-long-random-32-char-minimum-secret
JWT_EXPIRATION=1h
35 changes: 35 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
FROM node:20-alpine AS base
WORKDIR /app

COPY package*.json ./
RUN npm ci

FROM base AS development
ENV NODE_ENV=development
COPY tsconfig*.json ./
COPY jest.config.ts ./
COPY eslint.config.mjs ./
COPY src ./src
COPY tests ./tests

EXPOSE 3000
CMD ["npm", "run", "dev"]

FROM base AS build
ENV NODE_ENV=production
COPY tsconfig*.json ./
COPY src ./src
RUN npm run build && npm prune --omit=dev

FROM node:20-alpine AS production
WORKDIR /app
RUN apk add --no-cache tini curl

ENV NODE_ENV=production
COPY --from=build /app/package*.json ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist

EXPOSE 3000
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/server.js"]
40 changes: 40 additions & 0 deletions backend/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import js from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';

export default tseslint.config(
{ ignores: ['dist', 'node_modules', 'coverage'] },
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.ts'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
parserOptions: {
project: './tsconfig.json',
},
},
rules: {
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
disallowTypeAnnotations: false,
},
],
'@typescript-eslint/no-misused-promises': [
'error',
{
checksVoidReturn: {
attributes: false,
},
},
],
},
},
eslintConfigPrettier,
);
13 changes: 13 additions & 0 deletions backend/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Config } from 'jest';

const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests'],
moduleFileExtensions: ['ts', 'js', 'json'],
testMatch: ['**/*.spec.ts'],
collectCoverageFrom: ['src/**/*.ts', '!src/server.ts'],
clearMocks: true,
};

export default config;
Loading