Node.js REST API for apartment and office space rentals. Built with Express 5, Prisma ORM, and MySQL. Runs in Docker.
| Layer | Technology |
|---|---|
| Runtime | Node.js 20 |
| Framework | Express 5 |
| ORM | Prisma 6 |
| Database | MySQL 8.0 |
| Auth | JWT (jsonwebtoken) |
| Image processing | sharp (WebP conversion) |
| File upload | multer |
| Nodemailer + Brevo SMTP | |
| i18n | i18next (en, fi) |
| Container | Docker + Docker Compose |
| Linting | ESLint 9 (flat config) |
| CI/CD | GitHub Actions |
jussispace-backend/
├── src/
│ ├── server.js # Entry point — starts HTTP server
│ ├── app.js # Express app (middleware, routes, static files)
│ ├── i18n.js # i18next setup
│ ├── routes/
│ │ ├── index.js # Mounts all routes under /api
│ │ ├── auth.js
│ │ ├── users.js
│ │ ├── properties.js # Includes image sub-routes
│ │ ├── amenities.js
│ │ └── orders.js
│ ├── controllers/
│ │ ├── auth.js # Register, login, me, logout, password reset
│ │ ├── users.js # User CRUD
│ │ ├── properties.js # Property CRUD
│ │ ├── propertyImages.js # Image upload, update, delete
│ │ ├── amenities.js # Amenity CRUD
│ │ └── orders.js # Order CRUD + approval workflow
│ ├── middleware/
│ │ ├── authenticate.js # JWT auth middleware
│ │ ├── authorize.js # Role-based access control
│ │ ├── upload.js # multer config (memory storage, 10 MB limit)
│ │ └── errorHandler.js # Centralised error handler
│ ├── mail/
│ │ ├── mailer.js # Nodemailer transport (Brevo SMTP)
│ │ └── templates/
│ │ ├── welcome.js
│ │ ├── resetPassword.js
│ │ └── passwordChanged.js
│ └── locales/
│ ├── en/translation.json
│ └── fi/translation.json
├── prisma/
│ ├── schema.prisma # User, Property, PropertyImage, Amenity, Order
│ └── seed.js # Seeds users, amenities, properties, orders
├── scripts/
│ ├── deploy.js # Tag and push a production release
│ ├── generate-postman.js # Generates Postman collection
│ ├── sync-translations.js # Syncs missing translation keys
│ └── user.js # CLI user management tool
├── uploads/
│ └── properties/ # Uploaded images (WebP) — gitignored
├── postman/
│ └── jussispace.postman_collection.json
├── .github/
│ └── workflows/
│ ├── ci.yml # CI: lint, security audit, Docker build
│ └── deploy.yml # CD: build, push to Docker Hub, deploy to EC2
├── Dockerfile # 3-stage multi-stage build
├── docker-compose.yml # Local dev — app + mysql
├── docker-compose.prod.yml # Production — uses Docker Hub image
├── dev # Developer command shortcuts
├── eslint.config.mjs
├── .env.example # Local environment variable template
├── .env.production.example # Production environment variable template
├── CHANGELOG.md
└── .dockerignore
- Node.js 20+ (use
nvm useto switch — version pinned in.nvmrc) - Docker + Docker Compose
# Build and start all services (MySQL + app)
./dev restart
# View logs
./dev logsnpm install
cp .env.example .env
# Edit .env — set DATABASE_URL to point to your local MySQL instance,
# and set JWT_SECRET and mail variablesSet up the database:
./dev db:generate # generate Prisma client
./dev db:push # create tables from schema
./dev db:seed # populate with test dataStart the server:
./dev dev # or: npm run devThe API is available at http://localhost:3000.
| Environment | URL |
|---|---|
| Local dev | http://localhost:3000/api |
| Production | https://backend-lab-jussispace.jussialanen.com/api |
Copy .env.example to .env for local dev, or .env.production.example to .env on the EC2 server.
| Variable | Default | Description |
|---|---|---|
NODE_ENV |
development |
Runtime environment |
PORT |
3000 |
HTTP port |
DATABASE_URL |
— | Prisma MySQL connection string |
JWT_SECRET |
— | Secret key for signing JWT tokens |
JWT_EXPIRES_IN |
7d |
JWT token expiry duration |
ALLOWED_ORIGINS |
* |
Comma-separated CORS origins |
FRONTEND_URL |
— | Frontend base URL (used in password reset emails) |
MAIL_ENABLED |
false |
Set to true to enable email sending |
MAIL_HOST |
— | SMTP host |
MAIL_PORT |
587 |
SMTP port |
MAIL_USERNAME |
— | SMTP username |
MAIL_PASSWORD |
— | SMTP password |
MAIL_FROM_ADDRESS |
— | Sender email address |
MAIL_FROM_NAME |
— | Sender display name |
MYSQL_ROOT_PASSWORD |
— | MySQL root password (production) |
MYSQL_DATABASE |
— | MySQL database name (production) |
MYSQL_USER |
— | MySQL user (production) |
MYSQL_PASSWORD |
— | MySQL user password (production) |
Important: Always set a strong
JWT_SECRETbefore deploying to production.
See API.md for full endpoint documentation.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/ |
— | API title and description |
| GET | /api/health |
— | Health check |
| GET | /api/translations |
— | i18n strings (?lang=en|fi) |
| POST | /api/auth/register |
— | Register |
| POST | /api/auth/login |
— | Login (returns JWT) |
| GET | /api/auth/me |
Bearer | Current user |
| POST | /api/auth/logout |
Bearer | Logout |
| POST | /api/auth/lost-password |
— | Request password reset |
| POST | /api/auth/reset-password |
— | Complete password reset |
| GET | /api/users |
admin | List users |
| GET/PUT/DELETE | /api/users/:id |
self or admin | Manage user |
| GET | /api/properties |
— | List properties (?type, ?city, ?status) |
| GET | /api/properties/:id |
— | Get property |
| POST | /api/properties |
admin/editor | Create property |
| PUT/DELETE | /api/properties/:id |
admin/editor | Update / delete |
| GET | /api/properties/:id/images |
— | List images |
| POST | /api/properties/:id/images |
admin/editor | Upload image (→ WebP) |
| PUT/DELETE | /api/properties/:id/images/:imageId |
admin/editor | Update / delete image |
| GET | /api/amenities |
— | List amenities |
| GET | /api/amenities/:id |
— | Get amenity |
| POST | /api/amenities |
admin/editor | Create amenity |
| PUT/DELETE | /api/amenities/:id |
admin/editor | Update / delete |
| GET | /api/orders |
admin | List orders (?status, ?userId, ?propertyId) |
| GET | /api/orders/:id |
self or admin | Get order |
| POST | /api/orders |
Bearer | Create order |
| PUT | /api/orders/:id |
admin | Approve / decline order |
| DELETE | /api/orders/:id |
self or admin | Delete order |
| Model | Table | Description |
|---|---|---|
User |
users |
Application users with role-based access |
Property |
properties |
Apartment and office listings |
PropertyImage |
property_images |
Multiple images per property (WebP) |
Amenity |
amenities |
Reusable feature tags |
PropertyAmenity |
property_amenities |
Property ↔ amenity join table |
Order |
orders |
Rental bookings with approval workflow |
./dev db:generate # generate Prisma client after schema changes
./dev db:migrate # create and apply a new migration
./dev db:push # push schema directly (dev only, no migration file)
./dev db:seed # seed test data
./dev db:studio # open Prisma Studio GUIRun ./dev db:seed to populate the database with test data:
| What | Count | Details |
|---|---|---|
| Users | 2 | superadmin@example.com (admin), tenant@example.com (subscriber) |
| Amenities | 12 | WiFi, Parking, Sauna, Elevator, AC, etc. |
| Properties | 9 | 5 apartments + 4 offices in Helsinki and Espoo |
| Orders | 4 | Covers all statuses: approved, pending, declined |
All seed passwords: password123
Images are uploaded via POST /api/properties/:id/images as multipart/form-data with the field name image.
- Accepted formats: JPEG, PNG, WebP, GIF (max 10 MB)
- Automatically converted to WebP at 82% quality via
sharp - Stored at
/uploads/properties/{propertyId}/{uuid}.webp - Publicly accessible at
http://localhost:3000/uploads/properties/{propertyId}/{uuid}.webp - Setting
isPrimary: trueunsets the primary flag on all other images for that property - Deleting an image removes the file from disk
In production (EC2), the uploads directory is a named Docker volume — files persist across deployments.
| Stage | Purpose |
|---|---|
deps |
Installs production dependencies only (npm ci --omit=dev) |
build |
Installs all deps and runs prisma generate |
production |
Copies prod node_modules + Prisma client, creates uploads dir, runs as non-root user |
| Service | Image | Port |
|---|---|---|
mysql |
mysql:8.0 |
3306 |
app |
Built from Dockerfile |
3000 |
Used on EC2. Pulls the pre-built image from Docker Hub instead of building locally. Named volumes keep MySQL data and uploaded images across deployments.
# First boot on EC2
IMAGE_TAG=latest DOCKER_USERNAME=yourname docker compose -f docker-compose.prod.yml up -dDeployments are triggered by pushing a git tag in the format x-x-x-production.
# Using the dev script (prompts for version if omitted)
./dev deploy 1-0-0
./dev deploy # interactive promptThis creates and pushes the tag 1-0-0-production, which triggers the GitHub Actions pipeline:
- Builds the Docker image
- Pushes
yourname/jussispace-backend:1-0-0-production+:latestto Docker Hub - SSHs into EC2, pulls the new image, restarts the app container
- Runs
prisma db pushfor schema migrations - Prunes old images to free disk space
You can also trigger a deploy manually from GitHub → Actions → Build and Deploy → Run workflow.
| Secret | Description |
|---|---|
DOCKER_USERNAME |
Docker Hub username |
DOCKER_PASSWORD |
Docker Hub access token |
EC2_HOST |
EC2 public IP address |
EC2_SSH_KEY |
Contents of your .pem file |
source ./dev # load aliases into your shell session
./dev <command> # run a single command directly| Command | Description |
|---|---|
dev |
Sync schema, seed DB, start nodemon |
start |
node src/server.js (production mode) |
lint |
Run ESLint |
lint:fix |
Run ESLint with auto-fix |
install |
npm install |
postman |
Regenerate Postman collection |
i18n:sync |
Add missing translation keys |
user |
CLI user management (list/create/update/delete) |
deploy |
Tag and push a production release |
db:generate |
prisma generate |
db:migrate |
prisma migrate dev |
db:push |
prisma db push |
db:seed |
Seed the database |
db:studio |
Open Prisma Studio |
up |
docker compose up -d |
down |
docker compose down |
restart |
Rebuild and restart containers |
build |
docker compose build |
logs |
docker compose logs -f |
ps |
docker compose ps |
shell |
Shell inside the app container |
dbshell |
MySQL CLI inside the mysql container |
clean |
Remove containers and volumes (destructive) |
Triggered on push and pull requests to main / master.
| Job | What it checks |
|---|---|
| Lint | ESLint across src/ |
| Security Audit | High-severity vulnerabilities in production deps |
| Docker Build | Image builds successfully |
Triggered by a x-x-x-production git tag or manual dispatch.
| Job | What it does |
|---|---|
| Build & Push | Builds Docker image, pushes to Docker Hub |
| Deploy | SSHs into EC2, pulls image, restarts app, runs migrations |
- Helmet — secure HTTP response headers
- CORS — configurable allowed origins via
ALLOWED_ORIGINS - Rate limiting — 100 requests per 15 minutes per IP
- Body size limit — JSON payloads capped at 10 KB
- JWT authentication — stateless, configurable expiry
- Role-based access control —
admin,editor,subscriber - Password hashing — bcrypt with 12 salt rounds; never returned by the API
- Password reset tokens — cryptographically random, expire after 1 hour, single-use
- Non-root Docker user — container runs as
appuser - npm audit in CI — high-severity vulnerabilities block the pipeline