Portfolio
Loading Shiham's portfolio
Preparing the latest projects, skills, and contact paths with a quiet production-ready polish.
Initializing portfolio
Projects · Skills · Contact
Portfolio
Preparing the latest projects, skills, and contact paths with a quiet production-ready polish.
Initializing portfolio
Projects · Skills · Contact
A containerized Node.js API for practising authentication, PostgreSQL persistence, automated testing, and CI/CD workflows across local and Docker environments.
DevOps Playground API is a personal learning project built to explore how a small backend service can be prepared for consistent local, containerized, and CI environments. It combines a practical user-management API with operational concerns such as environment separation, database migrations, health checks, logging, and automated quality checks.
The Express service provides public status routes, sign-up, sign-in, sign-out, and protected user-management operations. JWTs are stored in HTTP-only cookies, access is controlled through admin and ownership checks, input is validated with Zod, passwords are hashed with bcrypt, and data is persisted to PostgreSQL through Drizzle ORM.
The repository uses a multi-stage Dockerfile, separate development and production-style Compose definitions, Neon Local for containerized development, and Neon Cloud-compatible configuration. GitHub Actions workflows run tests, linting, and formatting checks and can build multi-platform images for Docker Hub. I designed and implemented the project independently.
Related projects
Jan 2026 – Jun 2026
A multi-vendor commerce platform connecting buyers, sellers, and administrators through dedicated storefronts, dashboards, payments, chat, and personalized discovery.
Clients send HTTP requests to an Express application where security, parsing, cookie, and logging middleware run before route handlers. Routes delegate to controllers and services, which access Neon PostgreSQL through Drizzle ORM; Docker Compose and GitHub Actions provide local orchestration and automated validation and publishing.
devops-playground-api is a Node.js and Express API packaged as a DevOps learning project. The API is intentionally small, but the repository is set up to practice production-style backend concerns: Docker images, Docker Compose, environment files, database migrations, logging, testing, linting, formatting, and GitHub Actions CI/CD.
From the application perspective, this project exposes a basic user API:
From the DevOps perspective, the same API is used as a practice surface for:
This repository is not only a backend API. It is a playground for learning how backend code is prepared to run consistently across local machines, containers, and CI/CD.
Key learning goals:
Dockerfile can support development and production targets..env, .env.development, and .env.production separate runtime settings.Application request flow:
Client
-> Express app
-> Helmet / CORS / JSON parsing / cookie parsing
-> Morgan request logging
-> Arcjet security middleware
-> Routes
-> Controllers
-> Services
-> Drizzle ORM
-> Neon PostgreSQL
Development Docker flow:
Host machine
-> docker-compose.dev.yml
-> app container: devops-playground-api:dev
-> neon-local container: neondatabase/neon_local
-> Neon branch/database
Important files:
src/app.js: Express application setup and route mounting.src/server.js: starts the HTTP server.src/models/user.model.js: Drizzle user table schema.drizzle/: generated SQL migration files and metadata.Dockerfile: development and production image targets.docker-compose.dev.yml: development app plus Neon Local.docker-compose.prod.yml: production-style app container.Install these before running the project:
scripts/*.sh on Windows.The repository includes .env.example as the safe template. Real env files are ignored by Git.
Common env files:
.env: local development without Docker..env.development: Docker Compose development with Neon Local..env.production: production-style Compose run with Neon Cloud.| Variable | Required for | Purpose |
|---|---|---|
PORT | all runs | Host port for the API, default 3000. |
NODE_ENV | all runs | Runtime mode such as development, test, or production. |
LOG_LEVEL | all runs | Winston log level, for example info, debug, or error. |
DATABASE_URL | all runs | PostgreSQL connection string used by Drizzle and Neon. |
Never commit real .env, .env.development, or .env.production files.
Use this path when you want to run Node directly on your host machine while connecting to a Neon cloud database.
npm install
cp .env.example .env
Edit .env:
PORT=3000
NODE_ENV=development
USE_NEON_LOCAL=false
DATABASE_URL=your-neon-cloud-connection-string
JWT_SECRET=your-long-random-secret
ARCJET_KEY=your-arcjet-key
Apply migrations and start the API:
npm run db:migrate
npm run dev
Open:
http://localhost:3000
http://localhost:3000/health
http://localhost:3000/api
Host-local development should leave USE_NEON_LOCAL=false. Neon Local routing is intended for the Compose development app container.
Use this path to run the app container with hot reload and a Neon Local sidecar.
cp .env.example .env.development
Fill in .env.development with your Neon and Arcjet values. The development Compose file sets these for the app container:
USE_NEON_LOCAL=true
NEON_LOCAL_HOST=neon-local
Start the development environment through the script:
npm run dev:docker
Or run the steps manually:
docker compose -f docker-compose.dev.yml up --build -d
npm run db:migrate:dev
Useful development commands:
docker compose -f docker-compose.dev.yml logs -f app
docker compose -f docker-compose.dev.yml logs -f neon-local
docker compose -f docker-compose.dev.yml down
Development Compose behavior:
development target from Dockerfile.npm run dev./app for hot reload./app/node_modules.logs/ into /app/logs.neondatabase/neon_local:latest as neon-local.Use this path to practice a production-style local deployment with a Neon Cloud database.
cp .env.example .env.production
Edit .env.production:
NODE_ENV=production
PORT=3000
LOG_LEVEL=info
USE_NEON_LOCAL=false
DATABASE_URL=your-neon-production-connection-string
JWT_SECRET=your-production-secret
ARCJET_KEY=your-production-arcjet-key
ARCJET_ENV=production
Start through the script:
npm run prod:docker
Or run manually:
docker compose -f docker-compose.prod.yml up --build -d
npm run db:migrate:prod
Useful production commands:
docker logs -f devops-playground-api-prod
docker compose -f docker-compose.prod.yml down
Production Compose behavior:
production target from Dockerfile.npm start.logs/ into /app/logs./health.unless-stopped.The schema source is:
src/models/user.model.js
Generated migrations are stored in:
drizzle/
Migration commands:
npm run db:generate
npm run db:migrate
npm run db:generate:dev
npm run db:migrate:dev
npm run db:migrate:prod
npm run db:studio
Typical flow:
src/models/.npm run db:generate.drizzle/.Base URL:
http://localhost:3000
Public endpoints:
| Method | Path | Description |
|---|---|---|
GET | / | Plain text API greeting. |
GET | /health | Health status, timestamp, and process uptime. |
GET | /api | JSON API status message. |
Auth endpoints:
| Method | Path | Description |
|---|---|---|
POST | /api/auth/sign-up | Create a user and set the auth cookie. |
POST | /api/auth/sign-in | Authenticate and set the auth cookie. |
POST | /api/auth/sign-out | Clear the auth cookie. |
User endpoints:
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/users | Admin | List all users. |
GET | /api/users/:id | User/Admin | Get own profile, or any profile as admin. |
PUT | /api/users/:id | User/Admin | Update own profile, or any profile as admin. |
DELETE | /api/users/:id | Admin | Delete a user. |
Example sign-up:
curl -i -c cookies.txt \
-X POST http://localhost:3000/api/auth/sign-up \
-H "Content-Type: application/json" \
-d '{"name":"Admin User","email":"admin@example.com","password":"secret123","role":"admin"}'
Example authenticated request:
curl -b cookies.txt http://localhost:3000/api/users
Auth behavior:
JWT_SECRET.token.secure when NODE_ENV=production.sameSite is set to strict.Security middleware:
helmet() sets common HTTP security headers.cors() is currently open for learning convenience.cookie-parser reads the auth cookie.DRY_RUN outside production and LIVE in production.For a stricter production setup, replace open cors() with an explicit origin allowlist.
Health endpoint:
curl http://localhost:3000/health
Example response:
{
"status": "OK",
"timestamp": "2026-06-16T00:00:00.000Z",
"uptime": 12.34
}
Docker health checks:
Dockerfile defines a default health check against /health.docker-compose.prod.yml also defines a production health check.Logs:
logs/error.log and logs/combined.log.logs/ automatically if it does not exist.Useful commands:
docker compose -f docker-compose.dev.yml logs -f app
docker logs -f devops-playground-api-prod
Run tests:
npm test
The current Jest/Supertest suite checks:
GET /healthGET /apiOn Windows, if PowerShell blocks npm.ps1, use:
npm.cmd test
If Jest cannot write to the default temp cache directory, use a workspace cache:
npm.cmd test -- --runInBand --cacheDirectory=.cache/jest
The .cache/ directory is ignored by Git.
Run ESLint:
npm run lint
Fix auto-fixable lint issues:
npm run lint:fix
Check formatting:
npm run format:check
Write formatting changes:
npm run format
Workflows live in:
.github/workflows/
Workflows:
tests.yml: installs dependencies and runs Jest on pushes and pull requests to main and staging.lint-and-format.yml: runs ESLint and Prettier checks on pushes and pull requests to main and staging.docker-build-and-push.yml: builds and pushes Docker images on pushes to main and manual workflow dispatch.Required repository secrets:
| Secret | Workflow | Purpose |
|---|---|---|
TEST_DATABASE_URL | tests | Optional database URL for CI test runs. |
DOCKER_USERNAME | Docker publish | Docker Hub username. |
DOCKER_PASSWORD | Docker publish | Docker Hub password or access token. |
The test workflow also sets:
NODE_ENV=test
JWT_SECRET=test-secret-for-ci
LOG_LEVEL=error
Build a development image:
docker build --target development -t devops-playground-api:dev .
Build a production image:
docker build --target production -t devops-playground-api:latest .
Run the production Compose stack:
docker compose -f docker-compose.prod.yml up --build -d
The Docker publish workflow uses:
linux/amd64 and linux/arm64.latest, and timestamped production tags.Published image format:
docker.io/<DOCKER_USERNAME>/devops-playground-api:<tag>
Avoid sharing output from this command because it expands env-file values, including secrets:
docker compose -f docker-compose.dev.yml config
.
|-- .github/workflows/ # GitHub Actions CI/CD workflows
|-- drizzle/ # Drizzle migrations and metadata
|-- scripts/ # Bash helper scripts for dev/prod Compose runs
|-- src/
| |-- config/ # Database, Arcjet, and logger setup
| |-- controllers/ # Express route handlers
| |-- middleware/ # Auth and security middleware
| |-- models/ # Drizzle table definitions
| |-- routes/ # Express routers
| |-- services/ # Database-facing business logic
| |-- utils/ # JWT, cookie, and formatting helpers
| |-- validations/ # Zod schemas
| |-- app.js # Express app setup
| |-- index.js # Loads env and starts server
| `-- server.js # HTTP listen call
|-- tests/ # Jest and Supertest tests
|-- Dockerfile # Multi-stage image definition
|-- docker-compose.dev.yml # Development stack
|-- docker-compose.prod.yml # Production-style stack
|-- drizzle.config.js # Drizzle Kit config
|-- eslint.config.js # ESLint config
|-- jest.config.mjs # Jest config
|-- package.json # Scripts and dependencies
`-- README.md
Ignored local/runtime directories include:
node_modules/.env*.neon_local/.cache/coverage/logs/.idea/Wrong port:
3000.${PORT:-3000}:3000.PORT in your env file.Docker is not running:
docker info.PowerShell blocks npm:
npm.cmd run lint
npm.cmd test
Missing env file:
.env.example to .env, .env.development, or .env.production.Migrations fail:
DATABASE_URL points to the intended database.Neon Local does not start:
NEON_API_KEY, NEON_PROJECT_ID, and PARENT_BRANCH_ID.docker compose -f docker-compose.dev.yml logs -f neon-local..neon_local/ is writable and ignored by Git.Arcjet warnings during tests or local runs:
127.0.0.1 when a public IP is not available.Logs are missing:
logs/ automatically../logs to /app/logs.cors() for learning convenience. Production apps should restrict allowed origins.PUT /api/users/:id; add a dedicated password-change flow if needed.docker compose config is useful for debugging but prints expanded env values. Do not paste its output into public issues or logs.The case-study preview is collapsed. Activate the button to make the full content available.
Aug 2025 – Oct 2025
A marketplace for verified Sri Lankan homestays, supporting guest bookings, host onboarding, admin approval, and test-mode payments.
Oct 2025 – Nov 2025
A full-stack developer Q&A platform where programmers can ask, answer, vote, save questions, explore tags, and generate AI-assisted responses.
DATABASE_NAME| dev/prod docs |
Database name reference, commonly neondb. |
USE_NEON_LOCAL | Docker dev | Enables Neon Local driver routing when set to true. |
NEON_LOCAL_HOST | Docker dev | Neon Local service host, usually neon-local in Compose. |
JWT_SECRET | auth | Secret used to sign and verify JWT cookies. |
ARCJET_KEY | security | Arcjet project key. |
ARCJET_ENV | security docs | Environment label for Arcjet usage. |
NEON_API_KEY | Neon Local | Neon API key used by the Neon Local container. |
NEON_PROJECT_ID | Neon Local | Neon project ID. |
PARENT_BRANCH_ID | Neon Local | Parent branch used for local branch creation. |
DELETE_BRANCH | Neon Local | Whether Neon Local should delete the created branch. |