feat: add backend setup with Express and unit tests

Backend Foundation (Phase 1 - Step 1):

**Infrastructure:**
- Add backend service to docker-compose.yml
- Configure nginx to proxy /api/* to backend
- Node.js 20 Alpine Docker container

**Backend Setup:**
- Express.js REST API server
- CORS configuration
- Request logging middleware
- Error handling (404, 500)
- Graceful shutdown on SIGTERM/SIGINT
- Health check endpoint: GET /api/health

**Testing:**
- Jest + Supertest for unit tests
- 7 test cases covering:
  - Health check endpoint
  - 404 error handling
  - CORS headers
  - JSON body parsing
- Code coverage: 88.23%

**Project Structure:**
- backend/src/app.js - Express app setup
- backend/src/server.js - Server entry point
- backend/src/__tests__/ - Unit tests
- backend/README.md - Backend documentation

**Environment:**
- .env.example template
- Development configuration
- Ready for PostgreSQL integration

All tests passing 
This commit is contained in:
Radosław Gierwiało
2025-11-12 21:42:52 +01:00
parent a1357393e8
commit 320aaf1ce1
11 changed files with 5237 additions and 14 deletions

16
backend/.env.example Normal file
View File

@@ -0,0 +1,16 @@
# Server
NODE_ENV=development
PORT=3000
# Database (future)
# DATABASE_URL=postgresql://user:password@db:5432/spotlightcam
# JWT (future)
# JWT_SECRET=your-secret-key-here
# JWT_EXPIRES_IN=24h
# CORS
CORS_ORIGIN=http://localhost:8080
# WebRTC (future)
# STUN_SERVER=stun:stun.l.google.com:19302

29
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
# Dependencies
node_modules/
# Environment variables
.env
# Logs
logs/
*.log
npm-debug.log*
# Runtime data
pids/
*.pid
*.seed
# Coverage directory
coverage/
.nyc_output/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db

19
backend/Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM node:20-alpine
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy application files
COPY . .
# Expose port
EXPOSE 3000
# Start application
CMD ["npm", "run", "dev"]

127
backend/README.md Normal file
View File

@@ -0,0 +1,127 @@
# spotlight.cam Backend
Node.js + Express backend for spotlight.cam - P2P video exchange app for dance events.
## Features
- ✅ Express REST API
- ✅ CORS enabled
- ✅ Health check endpoint
- ✅ Error handling
- ✅ Unit tests (Jest + Supertest)
- ⏳ PostgreSQL integration (planned)
- ⏳ JWT authentication (planned)
- ⏳ Socket.IO for real-time chat (planned)
- ⏳ WebRTC signaling (planned)
## API Endpoints
### Health Check
- `GET /api/health` - Backend health status
### Future Endpoints
- `POST /api/auth/register` - Register new user
- `POST /api/auth/login` - Login user
- `GET /api/users/me` - Get current user
- `GET /api/events` - List events
- `POST /api/matches` - Create match
- `POST /api/ratings` - Rate partner
## Development
### Install dependencies
```bash
npm install
```
### Run in development mode
```bash
npm run dev
```
### Run tests
```bash
npm test
```
### Run tests in watch mode
```bash
npm run test:watch
```
### Run in production mode
```bash
npm start
```
## Environment Variables
Create a `.env` file (see `.env.example`):
```env
NODE_ENV=development
PORT=3000
CORS_ORIGIN=http://localhost:8080
```
## Project Structure
```
backend/
├── src/
│ ├── __tests__/ # Unit tests
│ │ └── app.test.js
│ ├── routes/ # API routes (future)
│ ├── controllers/ # Business logic (future)
│ ├── middleware/ # Custom middleware (future)
│ ├── utils/ # Helper functions (future)
│ ├── app.js # Express app setup
│ └── server.js # Server entry point
├── .env # Environment variables (gitignored)
├── .env.example # Environment variables template
├── package.json
└── Dockerfile
```
## Testing
Tests are written using:
- **Jest** - Test framework
- **Supertest** - HTTP assertions
Run tests:
```bash
npm test
```
Current test coverage:
- Health check endpoint
- 404 error handling
- CORS configuration
- JSON body parsing
## Docker
Build and run with Docker Compose (from project root):
```bash
docker compose up --build
```
Backend will be available at:
- Internal: http://backend:3000
- Through nginx: http://localhost:8080/api
## Next Steps
1. ✅ Basic Express setup
2. ✅ Health check endpoint
3. ✅ Unit tests
4. ⏳ PostgreSQL connection
5. ⏳ Database schema and migrations
6. ⏳ Authentication (JWT + bcrypt)
7. ⏳ Socket.IO for real-time chat
8. ⏳ WebRTC signaling
## License
TBD

4809
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
backend/package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "spotlightcam-backend",
"version": "1.0.0",
"description": "Backend API for spotlight.cam - P2P video exchange for dance events",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"test": "jest --coverage",
"test:watch": "jest --watch"
},
"keywords": ["webrtc", "p2p", "video", "dance", "matchmaking"],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1"
},
"devDependencies": {
"nodemon": "^3.0.2",
"jest": "^29.7.0",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": [
"/node_modules/"
],
"testMatch": [
"**/__tests__/**/*.test.js"
]
}
}

View File

@@ -0,0 +1,83 @@
const request = require('supertest');
const app = require('../app');
describe('Backend API Tests', () => {
describe('GET /api/health', () => {
it('should return 200 OK with health status', async () => {
const response = await request(app)
.get('/api/health')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveProperty('status', 'ok');
expect(response.body).toHaveProperty('message', 'Backend is running');
expect(response.body).toHaveProperty('timestamp');
expect(response.body).toHaveProperty('environment');
});
it('should return valid timestamp', async () => {
const response = await request(app)
.get('/api/health')
.expect(200);
const timestamp = new Date(response.body.timestamp);
expect(timestamp).toBeInstanceOf(Date);
expect(timestamp.getTime()).not.toBeNaN();
});
it('should return development environment in test mode', async () => {
const response = await request(app)
.get('/api/health')
.expect(200);
expect(response.body.environment).toBe('development');
});
});
describe('404 Handler', () => {
it('should return 404 for non-existent route', async () => {
const response = await request(app)
.get('/api/nonexistent')
.expect('Content-Type', /json/)
.expect(404);
expect(response.body).toHaveProperty('error', 'Not Found');
expect(response.body).toHaveProperty('message');
expect(response.body.message).toContain('Cannot GET /api/nonexistent');
});
it('should return 404 for POST to non-existent route', async () => {
const response = await request(app)
.post('/api/nonexistent')
.expect('Content-Type', /json/)
.expect(404);
expect(response.body).toHaveProperty('error', 'Not Found');
expect(response.body.message).toContain('Cannot POST /api/nonexistent');
});
});
describe('CORS Configuration', () => {
it('should have CORS headers', async () => {
const response = await request(app)
.get('/api/health')
.expect(200);
expect(response.headers).toHaveProperty('access-control-allow-origin');
});
});
describe('JSON Body Parser', () => {
it('should parse JSON body (when routes are added)', async () => {
// This will be tested when we add POST routes
// For now, just verify the middleware is loaded
const response = await request(app)
.post('/api/health')
.send({ test: 'data' })
.set('Content-Type', 'application/json')
.expect(404); // 404 because POST /api/health doesn't exist
expect(response.body).toHaveProperty('error');
});
});
});

54
backend/src/app.js Normal file
View File

@@ -0,0 +1,54 @@
const express = require('express');
const cors = require('cors');
const app = express();
// Middleware
app.use(cors({
origin: process.env.CORS_ORIGIN || 'http://localhost:8080',
credentials: true
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Request logging middleware
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
next();
});
// Health check endpoint
app.get('/api/health', (req, res) => {
res.status(200).json({
status: 'ok',
message: 'Backend is running',
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV || 'development'
});
});
// API routes (future)
// app.use('/api/auth', require('./routes/auth'));
// app.use('/api/users', require('./routes/users'));
// app.use('/api/events', require('./routes/events'));
// app.use('/api/matches', require('./routes/matches'));
// app.use('/api/ratings', require('./routes/ratings'));
// 404 handler
app.use((req, res) => {
res.status(404).json({
error: 'Not Found',
message: `Cannot ${req.method} ${req.path}`
});
});
// Error handler
app.use((err, req, res, next) => {
console.error('Error:', err);
res.status(err.status || 500).json({
error: err.message || 'Internal Server Error',
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});
module.exports = app;

31
backend/src/server.js Normal file
View File

@@ -0,0 +1,31 @@
require('dotenv').config();
const app = require('./app');
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, '0.0.0.0', () => {
console.log('=================================');
console.log('🚀 spotlight.cam Backend Started');
console.log('=================================');
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
console.log(`Server running on port: ${PORT}`);
console.log(`Health check: http://localhost:${PORT}/api/health`);
console.log('=================================');
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully...');
server.close(() => {
console.log('Server closed');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully...');
server.close(() => {
console.log('Server closed');
process.exit(0);
});
});

View File

@@ -9,6 +9,7 @@ services:
- ./nginx/conf.d:/etc/nginx/conf.d:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro
depends_on: depends_on:
- frontend - frontend
- backend
restart: unless-stopped restart: unless-stopped
frontend: frontend:
@@ -27,3 +28,19 @@ services:
stdin_open: true stdin_open: true
tty: true tty: true
command: npm run dev command: npm run dev
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: spotlightcam-backend
expose:
- "3000"
volumes:
- ./backend:/app
- /app/node_modules
environment:
- NODE_ENV=development
- PORT=3000
- CORS_ORIGIN=http://localhost:8080
restart: unless-stopped

View File

@@ -2,10 +2,9 @@ upstream frontend {
server frontend:5173; server frontend:5173;
} }
# Backend będzie dodany później upstream backend {
# upstream backend { server backend:3000;
# server backend:3000; }
# }
server { server {
listen 80; listen 80;
@@ -34,16 +33,21 @@ server {
proxy_read_timeout 60s; proxy_read_timeout 60s;
} }
# Backend API (do dodania później) # Backend API
# location /api { location /api {
# proxy_pass http://backend; proxy_pass http://backend;
# proxy_http_version 1.1; proxy_http_version 1.1;
#
# proxy_set_header Host $host; proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
# }
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# WebSocket dla Socket.IO (do dodania później) # WebSocket dla Socket.IO (do dodania później)
# location /socket.io { # location /socket.io {