Initial commit - Node.js API server

This commit is contained in:
Lalit Mohan Kalpasi 2026-01-06 20:13:19 +05:30
commit bc745ad172
7 changed files with 552 additions and 0 deletions

48
.dockerignore Normal file
View File

@ -0,0 +1,48 @@
# Git
.git
.gitignore
# Dependencies (will be installed in Docker)
node_modules
# Development files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.npm
# IDE
.idea/
.vscode/
*.swp
*.swo
*.sublime-*
# Environment files (sensitive)
.env
.env.local
.env.*.local
*.env
# Test files
coverage/
.nyc_output/
*.test.js
*.spec.js
__tests__/
# Documentation
README.md
CHANGELOG.md
docs/
# OS
.DS_Store
Thumbs.db
# Misc
*.log
*.pid
*.seed
*.pid.lock

33
.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
# Dependencies
node_modules/
# Environment files
.env
.env.local
.env.*.local
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Test coverage
coverage/
.nyc_output/
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Build artifacts
dist/
build/

47
Dockerfile Normal file
View File

@ -0,0 +1,47 @@
# ==========================================
# Production-Ready Multi-Stage Dockerfile
# ==========================================
# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install production dependencies only
RUN npm ci --only=production && npm cache clean --force
# ==========================================
# Stage 2: Production Image
# ==========================================
FROM node:20-alpine AS runner
WORKDIR /app
# Set production environment
ENV NODE_ENV=production
ENV PORT=8080
# Create non-root user for security
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nodeuser
# Copy dependencies from deps stage
COPY --from=deps /app/node_modules ./node_modules
# Copy application code
COPY --chown=nodeuser:nodejs . .
# Switch to non-root user
USER nodeuser
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
# Start the application
CMD ["node", "index.js"]

99
README.md Normal file
View File

@ -0,0 +1,99 @@
# test-git-fcz5
Production-ready Node.js API server for testing git serverless deployment.
## Features
- ✅ Express.js REST API
- ✅ CRUD operations (Users & Tasks)
- ✅ Health checks (`/health`, `/ready`)
- ✅ Environment info endpoint (`/api/env`)
- ✅ Production-ready Dockerfile (multi-stage)
- ✅ Security middleware (Helmet, CORS)
- ✅ Request logging (Morgan)
- ✅ Gzip compression
## API Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/` | Service info |
| GET | `/health` | Health check |
| GET | `/ready` | Readiness check |
| GET | `/api/env` | Environment variables (for testing) |
| GET | `/api/users` | List all users |
| GET | `/api/users/:id` | Get user by ID |
| POST | `/api/users` | Create user |
| PUT | `/api/users/:id` | Update user |
| DELETE | `/api/users/:id` | Delete user |
| GET | `/api/tasks` | List tasks (filter by userId, status) |
| POST | `/api/tasks` | Create task |
| PATCH | `/api/tasks/:id/status` | Update task status |
| ALL | `/api/echo` | Echo request details |
## Local Development
```bash
# Install dependencies
npm install
# Run in development mode
npm run dev
# Run in production mode
npm start
```
## Docker
```bash
# Build image
docker build -t test-git-fcz5 .
# Run container
docker run -p 8080:8080 test-git-fcz5
```
## Testing API
```bash
# Health check
curl http://localhost:8080/health
# Get environment info
curl http://localhost:8080/api/env
# List users
curl http://localhost:8080/api/users
# Create user
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name": "Test User", "email": "test@example.com"}'
# Create task
curl -X POST http://localhost:8080/api/tasks \
-H "Content-Type: application/json" \
-d '{"userId": "1", "title": "New task"}'
# Update task status
curl -X PATCH http://localhost:8080/api/tasks/1/status \
-H "Content-Type: application/json" \
-d '{"status": "done"}'
```
## Backward Compatibility Testing
This app uses the **OLD boltic.yaml format** (no `serverlessConfig` section).
### Test Steps:
1. Deploy this app BEFORE new conductor code
2. Configure from UI: Scaling, Env, PortMap
3. Deploy new conductor code
4. Push a code change
5. Verify UI settings are preserved
## License
MIT

30
boltic.yaml Normal file
View File

@ -0,0 +1,30 @@
# Git Serverless Configuration - OLD FORMAT
# For backward compatibility testing (no serverlessConfig)
app: "test-git-fcz5"
region: "asia-south1"
handler: "handler.handler"
language: "nodejs/20"
settings:
nodejs:
common_js: false
build:
builtin: dockerfile
ignorefile: .dockerignore
no_cache: false
args:
NODE_ENV: "production"
BUILD_VERSION: "1.0.0"
# NOTE: NO serverlessConfig section!
# This is the OLD format for backward compatibility testing.
#
# TESTING STEPS:
# 1. Deploy this app BEFORE deploying new conductor code
# 2. Configure Scaling/Env/PortMap from UI
# 3. Deploy new conductor code
# 4. Push a code change to this repo
# 5. Verify all UI settings are preserved

255
index.js Normal file
View File

@ -0,0 +1,255 @@
/**
* Production-Ready Node.js API Server
* For testing git serverless deployment
*/
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const compression = require('compression');
const { v4: uuidv4 } = require('uuid');
// Load environment variables
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 8080;
const NODE_ENV = process.env.NODE_ENV || 'development';
// ===========================================
// Middleware
// ===========================================
app.use(helmet()); // Security headers
app.use(cors()); // CORS support
app.use(compression()); // Gzip compression
app.use(morgan('combined')); // Request logging
app.use(express.json()); // Parse JSON bodies
app.use(express.urlencoded({ extended: true }));
// ===========================================
// In-Memory Database (for testing)
// ===========================================
const database = {
users: [
{ id: '1', name: 'John Doe', email: 'john@example.com', createdAt: new Date().toISOString() },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', createdAt: new Date().toISOString() },
],
tasks: [
{ id: '1', userId: '1', title: 'Complete project', status: 'pending', createdAt: new Date().toISOString() },
{ id: '2', userId: '1', title: 'Review PR', status: 'done', createdAt: new Date().toISOString() },
]
};
// ===========================================
// Health Check Routes
// ===========================================
app.get('/', (req, res) => {
res.json({
service: 'test-git-fcz5',
version: '1.0.0',
status: 'healthy',
timestamp: new Date().toISOString(),
environment: NODE_ENV,
});
});
app.get('/health', (req, res) => {
res.json({
status: 'ok',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
});
});
app.get('/ready', (req, res) => {
res.json({ ready: true });
});
// ===========================================
// Environment Info Route (for testing)
// ===========================================
app.get('/api/env', (req, res) => {
res.json({
message: 'Environment variables (for testing config overrides)',
environment: {
NODE_ENV: process.env.NODE_ENV || 'not-set',
PORT: process.env.PORT || 'not-set',
// Boltic system variables
BOLT_APPLICATION_NAME: process.env.BOLT_APPLICATION_NAME || 'not-set',
BOLT_APPLICATION_SLUG: process.env.BOLT_APPLICATION_SLUG || 'not-set',
BOLT_APPLICATION_REGION_ID: process.env.BOLT_APPLICATION_REGION_ID || 'not-set',
// Custom env vars (set from UI or boltic.yaml)
API_KEY: process.env.API_KEY ? '***hidden***' : 'not-set',
DATABASE_URL: process.env.DATABASE_URL ? '***hidden***' : 'not-set',
CUSTOM_VAR: process.env.CUSTOM_VAR || 'not-set',
UI_TEST_VAR: process.env.UI_TEST_VAR || 'not-set',
},
testInfo: {
purpose: 'Verify environment variables from UI and boltic.yaml',
configFormat: 'Check if serverlessConfig overrides are applied',
}
});
});
// ===========================================
// Users API
// ===========================================
app.get('/api/users', (req, res) => {
res.json({
success: true,
data: database.users,
total: database.users.length,
});
});
app.get('/api/users/:id', (req, res) => {
const user = database.users.find(u => u.id === req.params.id);
if (!user) {
return res.status(404).json({ success: false, error: 'User not found' });
}
res.json({ success: true, data: user });
});
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ success: false, error: 'Name and email are required' });
}
const newUser = {
id: uuidv4(),
name,
email,
createdAt: new Date().toISOString(),
};
database.users.push(newUser);
res.status(201).json({ success: true, data: newUser });
});
app.put('/api/users/:id', (req, res) => {
const index = database.users.findIndex(u => u.id === req.params.id);
if (index === -1) {
return res.status(404).json({ success: false, error: 'User not found' });
}
database.users[index] = { ...database.users[index], ...req.body };
res.json({ success: true, data: database.users[index] });
});
app.delete('/api/users/:id', (req, res) => {
const index = database.users.findIndex(u => u.id === req.params.id);
if (index === -1) {
return res.status(404).json({ success: false, error: 'User not found' });
}
const deleted = database.users.splice(index, 1);
res.json({ success: true, data: deleted[0] });
});
// ===========================================
// Tasks API
// ===========================================
app.get('/api/tasks', (req, res) => {
const { userId, status } = req.query;
let tasks = [...database.tasks];
if (userId) tasks = tasks.filter(t => t.userId === userId);
if (status) tasks = tasks.filter(t => t.status === status);
res.json({
success: true,
data: tasks,
total: tasks.length,
});
});
app.post('/api/tasks', (req, res) => {
const { userId, title } = req.body;
if (!userId || !title) {
return res.status(400).json({ success: false, error: 'userId and title are required' });
}
const newTask = {
id: uuidv4(),
userId,
title,
status: 'pending',
createdAt: new Date().toISOString(),
};
database.tasks.push(newTask);
res.status(201).json({ success: true, data: newTask });
});
app.patch('/api/tasks/:id/status', (req, res) => {
const { status } = req.body;
const validStatuses = ['pending', 'in-progress', 'done'];
if (!status || !validStatuses.includes(status)) {
return res.status(400).json({
success: false,
error: `Invalid status. Must be one of: ${validStatuses.join(', ')}`
});
}
const task = database.tasks.find(t => t.id === req.params.id);
if (!task) {
return res.status(404).json({ success: false, error: 'Task not found' });
}
task.status = status;
res.json({ success: true, data: task });
});
// ===========================================
// Echo/Debug Route (for testing)
// ===========================================
app.all('/api/echo', (req, res) => {
res.json({
method: req.method,
path: req.path,
query: req.query,
headers: req.headers,
body: req.body,
timestamp: new Date().toISOString(),
});
});
// ===========================================
// 404 Handler
// ===========================================
app.use((req, res) => {
res.status(404).json({
success: false,
error: 'Not Found',
path: req.path,
});
});
// ===========================================
// Error Handler
// ===========================================
app.use((err, req, res, next) => {
console.error('Error:', err);
res.status(500).json({
success: false,
error: NODE_ENV === 'production' ? 'Internal Server Error' : err.message,
});
});
// ===========================================
// Start Server
// ===========================================
app.listen(PORT, '0.0.0.0', () => {
console.log('========================================');
console.log(`🚀 Server started successfully!`);
console.log(`📍 Environment: ${NODE_ENV}`);
console.log(`🌐 Listening on: http://0.0.0.0:${PORT}`);
console.log(`💚 Health check: http://0.0.0.0:${PORT}/health`);
console.log('========================================');
});
module.exports = app;

40
package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "test-git-fcz5",
"version": "1.0.0",
"description": "Production-ready Node.js API server for testing",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest --coverage",
"lint": "eslint .",
"build": "echo 'No build step required'"
},
"keywords": [
"nodejs",
"api",
"express",
"production",
"boltic"
],
"author": "Boltic",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"helmet": "^7.1.0",
"morgan": "^1.10.0",
"compression": "^1.7.4",
"dotenv": "^16.3.1",
"uuid": "^9.0.1"
},
"devDependencies": {
"nodemon": "^3.0.2",
"jest": "^29.7.0",
"eslint": "^8.56.0"
}
}