Episode 3 — NodeJS MongoDB Backend Architecture / 3.13 — Production Project Structure
3.13.b --- File Naming and Git Configuration
In one sentence: Consistent file naming and disciplined Git configuration are the invisible backbone of team productivity --- they eliminate bikeshedding, prevent accidental secret leaks, and make every commit message a searchable log entry.
Navigation: <- 3.13.a Application Structure | 3.13.c --- Configuration Management ->
1. File naming conventions
File naming may seem trivial, but inconsistency creates real friction: developers waste time guessing whether the user model is User.js, user.js, user.model.js, or userModel.js.
The three common styles
| Convention | Example | When to use |
|---|---|---|
| kebab-case | user-profile.js | Most common in Node.js projects; file systems treat it consistently |
| camelCase | userProfile.js | Matches JavaScript variable naming; popular in some teams |
| PascalCase | UserProfile.js | Classes and React components; signals "this exports a class" |
The case-sensitivity trap
macOS and Windows file systems are case-insensitive by default. This means User.js and user.js are the same file locally but different files on Linux servers (where your code deploys). This causes mysterious "module not found" errors in production.
Rule: Pick one convention and enforce it. kebab-case is the safest choice for Node.js because it avoids case-sensitivity issues entirely.
2. Naming patterns by file type
A suffix-based naming pattern makes every file's role instantly clear:
| File type | Pattern | Example |
|---|---|---|
| Model | <resource>.model.js | user.model.js |
| Controller | <resource>.controller.js | user.controller.js |
| Service | <resource>.service.js | user.service.js |
| Route | <resource>.routes.js | user.routes.js |
| Validator | <resource>.validator.js | user.validator.js |
| Middleware | <name>.middleware.js | auth.middleware.js |
| Test | <name>.test.js or <name>.spec.js | user.service.test.js |
| Config | <name>.config.js | ecosystem.config.js |
Alternative: PascalCase for models only
Some teams use PascalCase for model files because Mongoose models export a PascalCase constructor:
// User.js (PascalCase file) --- exports `User` model
const User = require('../models/User');
// vs
// user.model.js (kebab-style with suffix) --- same export
const User = require('../models/user.model');
Both work. The key is consistency within a project. The suffix approach (.model.js) is more descriptive when you have many files open in your editor.
3. Git configuration: .gitignore essentials
The .gitignore file is your first line of defense against committing secrets, build artifacts, and bloat.
Complete .gitignore for Node.js projects
# Dependencies
node_modules/
package-lock.json # optional: some teams commit this, some don't
# Environment variables (NEVER commit secrets)
.env
.env.local
.env.*.local
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build output
dist/
build/
.next/
# Test coverage
coverage/
.nyc_output/
# OS files
.DS_Store
Thumbs.db
*.swp
*.swo
# IDE files
.vscode/
.idea/
*.sublime-project
*.sublime-workspace
# Uploaded files (keep the folder, ignore contents)
public/uploads/*
!public/uploads/.gitkeep
# Temporary files
tmp/
temp/
*.tmp
The .gitkeep trick
Git does not track empty directories. If you need an uploads/ folder to exist in the repo:
mkdir -p public/uploads
touch public/uploads/.gitkeep
Then in .gitignore:
public/uploads/*
!public/uploads/.gitkeep
This keeps the directory structure but ignores uploaded files.
4. .gitignore templates
GitHub maintains official templates at github.com/github/gitignore. The Node.js template covers the essentials. You can generate one when creating a new repository or use:
# Download the Node.js template
curl -o .gitignore https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
Always review and customise the template for your project's specific needs.
5. Git commit message conventions (Conventional Commits)
Conventional Commits is a specification that adds structure to commit messages, enabling automated changelogs and semantic versioning.
Format
<type>(<scope>): <description>
[optional body]
[optional footer]
Types
| Type | When to use | Example |
|---|---|---|
feat | New feature | feat(auth): add password reset endpoint |
fix | Bug fix | fix(user): correct email validation regex |
docs | Documentation only | docs(readme): add API setup instructions |
style | Formatting, no logic change | style(lint): fix eslint warnings |
refactor | Code change, no feature/fix | refactor(post): extract slug generation to utility |
test | Adding or updating tests | test(auth): add login integration tests |
chore | Build, config, tooling | chore(deps): update mongoose to v8 |
perf | Performance improvement | perf(query): add index to posts.slug field |
ci | CI/CD changes | ci(github): add Node.js 20 to test matrix |
revert | Revert a previous commit | revert: revert feat(auth) commit abc123 |
Good vs bad commit messages
| Bad | Good |
|---|---|
fix stuff | fix(auth): return 401 instead of 500 for expired tokens |
update | feat(post): add pagination to GET /posts endpoint |
WIP | wip(comment): partial implementation of nested replies |
changes | refactor(middleware): extract rate limiter to separate file |
Breaking changes
feat(api)!: change response format from array to paginated object
BREAKING CHANGE: GET /api/v1/posts now returns { data: [], meta: { page, limit, total } }
instead of a plain array. All clients must update their response parsing.
The ! after the scope and the BREAKING CHANGE footer both signal a major version bump.
6. Branch naming strategies
| Prefix | Purpose | Example |
|---|---|---|
feature/ | New functionality | feature/user-avatar-upload |
bugfix/ | Non-urgent bug fix | bugfix/email-validation-crash |
hotfix/ | Urgent production fix | hotfix/jwt-token-expiry |
release/ | Release preparation | release/v2.1.0 |
docs/ | Documentation updates | docs/api-endpoints |
refactor/ | Code restructuring | refactor/extract-services |
test/ | Test additions | test/auth-integration |
Branch naming rules
feature/short-descriptive-name # Good
feature/JIRA-123-add-user-search # Good (with ticket number)
Feature/MyNewThing # Bad (inconsistent casing)
new-feature # Bad (no prefix)
feature/this-is-a-very-long-branch-name-that-nobody-wants-to-type # Bad
Git flow (simplified)
main (production-ready)
└── develop (integration branch)
├── feature/user-auth
├── feature/post-crud
└── bugfix/login-error
7. .editorconfig for consistent formatting
Different editors have different default settings. .editorconfig ensures everyone on the team uses the same indentation, line endings, and charset --- regardless of their IDE.
# .editorconfig
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
What each setting does
| Setting | Value | Why |
|---|---|---|
indent_style | space | Consistent rendering across editors and GitHub |
indent_size | 2 | Node.js community standard |
end_of_line | lf | Prevents Windows \r\n from polluting diffs |
charset | utf-8 | Universal encoding |
trim_trailing_whitespace | true | Eliminates noise in diffs |
insert_final_newline | true | POSIX standard; prevents "no newline at end of file" warnings |
Most editors (VS Code, WebStorm, Vim) support .editorconfig natively or with a plugin.
8. Husky for Git hooks
Husky makes it easy to run scripts before Git operations --- ensuring code quality before code reaches the repository.
Installation and setup
# Install Husky
npm install -D husky
# Initialize Husky (creates .husky/ directory)
npx husky init
This creates a .husky/ directory and adds a prepare script to package.json:
{
"scripts": {
"prepare": "husky"
}
}
Pre-commit hook
The pre-commit hook runs before every commit. If it exits with a non-zero code, the commit is blocked.
# .husky/pre-commit
npm run lint
npm run format:check
Pre-push hook
The pre-push hook runs before every push. Use it for slower checks like tests.
# .husky/pre-push
npm test
Common hook scripts
| Hook | When it runs | Typical use |
|---|---|---|
pre-commit | Before git commit | Lint, format check, type check |
commit-msg | After writing commit message | Validate Conventional Commits format |
pre-push | Before git push | Run test suite |
post-merge | After git merge / git pull | Auto-run npm install if package.json changed |
Commit message validation with commitlint
npm install -D @commitlint/cli @commitlint/config-conventional
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
};
# .husky/commit-msg
npx --no -- commitlint --edit ${1}
Now any commit message that does not follow Conventional Commits format is rejected.
9. Putting it all together: project root files
project-root/
├── .editorconfig --- Editor consistency
├── .eslintrc.js --- Linting rules (see 3.13.e)
├── .gitignore --- Files Git should ignore
├── .husky/ --- Git hooks
│ ├── pre-commit
│ ├── pre-push
│ └── commit-msg
├── .prettierrc --- Formatting rules (see 3.13.e)
├── .env --- Local environment (NEVER committed)
├── .env.example --- Template for team (committed)
├── commitlint.config.js --- Commit message rules
├── package.json --- Project metadata and scripts
├── server.js --- Entry point
└── src/ --- Application code
10. Key takeaways
- Pick one naming convention (prefer
kebab-casewith.model.js/.controller.jssuffixes) and enforce it. .gitignoreis not optional --- commit it first, before any code. Always excludenode_modules/,.env, logs, and OS files.- Conventional Commits (
feat:,fix:,chore:) make your Git history searchable and enable automated tooling. - Branch prefixes (
feature/,bugfix/,hotfix/) communicate intent at a glance. .editorconfigeliminates formatting wars between developers using different editors.- Husky + commitlint automate quality gates so bad code and bad commit messages never reach the repository.
Explain-It Challenge
Explain without notes:
- Why is
kebab-casesafer thanPascalCasefor file names in Node.js projects deployed on Linux? - Write a
.gitignoreentry that keeps thepublic/uploads/directory but ignores all files inside it. - A commit message says
"fixed login". Rewrite it following Conventional Commits format and explain each part.
Navigation: <- 3.13.a Application Structure | 3.13.c --- Configuration Management ->