A full-stack user profile management app using React and Node.js. Users can create, view, edit, and delete profiles with profile pictures.
- Docker Compose
- Node.js (v20+)
- npm
- Clone and create a new branch
- Copy environment files:
backend/.env.example→backend/.envfrontend/.env.example→frontend/.env
- Install and run:
# Backend with Docker (first time) cd backend && npm install # If database already exists, clean it first docker-compose down -v # Start containers (database will initialize automatically) npm run docker:up # Wait for database to be ready, then run migrations npm run migrate # Frontend with Docker (new terminal) cd frontend && docker-compose up # OR run frontend without Docker cd frontend && npm install && npm start
- First run:
schema.sqlandinsert.sqlinitialize automatically - Subsequent runs: Use
npm run migratefor schema changes - To reset database completely:
cd backend && docker-compose down -v
What I did: Created separate service classes (UserService, FileService) instead of putting all logic in routes.
Why:
- Routes were getting messy with too much logic
- Wanted to reuse the same logic in different places
- Makes testing much easier - can test business logic separately
- When I need to change how users are created, I only touch one file
What I did: Built custom error classes and a central error handler that logs everything to a file.
Why:
- Got tired of inconsistent error responses across different endpoints
- Wanted all errors logged in one place for debugging
- Users should see helpful messages, not stack traces
- Each error type automatically gets the right HTTP status (404, 409, etc.)
What I did: Put all config in one file (backend/config/index.js).
Why:
- Hated searching for where a port or file size limit was defined
- All settings in one place makes changes easier
- Environment variables with good defaults
- Clear structure that's easy to understand
What I did: Added Helmet for headers, rate limiting, and strict validation.
Why:
- Helmet protects against common attacks (XSS, clickjacking) with minimal effort
- Rate limiting (100 requests per 15 min) prevents abuse
- File uploads are risky - strict type and size checks
- Input validation on both frontend and backend
What I did: Wrote 18+ tests covering services, routes, hooks, and components.
Why:
- Wanted confidence that refactoring wouldn't break things
- Service tests use mocked database, so they're fast
- Route tests use supertest to test actual HTTP requests
- Frontend tests ensure hooks and components work correctly
- Tests document how the code should behave
What I did: Split into ProfileForm, ProfileDetail, and reusable components (Toast, ConfirmModal).
Why:
- Each component has one job
- Toast and ConfirmModal are used everywhere - no duplication
- Easier to find bugs when components are small
- Form logic is complex - keeping it separate from display logic helps
What I did: Used node-pg-migrate for schema changes.
Why:
- Database changes need to be tracked like code changes
- Can roll back if something goes wrong
- Everyone on the team gets the same database structure
- Lightweight tool - just handles migrations, no heavy ORM
What I did: Plain CSS files, one per component.
Why:
- No extra dependencies or build steps
- Clear which styles belong to which component
- Easy to maintain with good naming
- SOCOTEC brand colors and logo integrated throughout
backend/
├── config/ # All configuration in one place
├── middlewares/ # Error handling, file upload, logging
├── routes/ # Thin route handlers
├── services/ # Business logic lives here
├── sql/ # Database and migrations
├── test/ # Tests for services and routes
├── uploads/ # Uploaded profile pictures
├── logs/ # Error logs
└── utils/ # Error classes and logger
frontend/
├── src/
│ ├── componenets/ # UI components
│ ├── constants/ # Validation rules and constants
│ ├── data/ # Countries and cities data
│ ├── hooks/ # Custom hooks
│ └── pages/ # Main pages
└── public/ # SOCOTEC logo
- Create: Form validates everything (names, email, phone, country, city)
- View: Shows all user details with profile picture
- Edit: Pre-fills form with existing data
- Delete: Asks for confirmation first
- Accepts JPEG, PNG, GIF, WebP (max 5MB)
- Shows preview before saving
- Validates on both client and server
- Automatically deletes old images when updating
- Clear error messages when validation fails
- Toast notifications for success/error
- All errors logged for debugging
- App keeps working even if something breaks
- Required: first name, last name, email
- Smart validation (email format, phone format, name patterns)
- Length limits on all fields
- City dropdown updates based on country selection
GET /api/users/:id- Get userPOST /api/users- Create user (with image)PUT /api/users/:id- Update user (with image)DELETE /api/users/:id- Delete userGET /health- Check if API is running
cd backend && npm testTests cover services and API routes with mocked database.
cd frontend && npm testTests cover custom hooks and UI components.
- Express.js - web server
- PostgreSQL - database
- Multer - file uploads
- Helmet - security headers
- Rate limiting - prevent abuse
- Mocha/Chai - testing
- React 17 - UI
- React Hook Form - form handling
- React Select - dropdowns
- Axios - API calls
- Jest - testing
- Security headers via Helmet
- Rate limiting (100 requests per 15 min)
- File type and size validation
- Input validation everywhere
- No stack traces sent to users
- SQL injection prevention (parameterized queries)
- User authentication and permissions
- Pagination for user lists
- Search and filtering
- CDN for images
- Image compression
- Audit trail for changes
- API documentation (Swagger)
- Pre-commit hooks with Prettier and ESLint
This was built with a focus on clean code, proper separation of concerns, and maintainability. I tried to keep things simple while still following best practices. The test coverage gives confidence for future changes, and the architecture makes it easy to add features without breaking existing functionality.
