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:
16
backend/.env.example
Normal file
16
backend/.env.example
Normal 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
29
backend/.gitignore
vendored
Normal 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
19
backend/Dockerfile
Normal 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
127
backend/README.md
Normal 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
4809
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
backend/package.json
Normal file
34
backend/package.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
83
backend/src/__tests__/app.test.js
Normal file
83
backend/src/__tests__/app.test.js
Normal 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
54
backend/src/app.js
Normal 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
31
backend/src/server.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user