# App Development Guide

This guide covers everything you need to know to build Node.js applications compatible with the Apps Manager. Follow these conventions and your app will work seamlessly both in local development and when deployed.

## Quick Start: Minimal Compatible App

The smallest possible app that works with the manager:

**package.json**
```json
{
  "name": "my-app",
  "version": "1.0.0",
  "scripts": {
    "start": "node index.js"
  },
  "engines": {
    "node": ">=18"
  }
}
```

**index.js**
```js
const http = require('http');

const PORT = process.env.PORT || 3000;

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello from my app!');
});

server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
```

That's it. This app runs locally with `npm start` and deploys without changes.

---

## package.json Contract

The manager reads your `package.json` to decide how to install, build, and run your app. Here is what each field triggers:

### Required Fields

#### `scripts.start` (required)

The command the manager uses to run your app. This is executed via PM2 as `npm start`.

```json
{
  "scripts": {
    "start": "node index.js"
  }
}
```

If no `start` script is found, the manager falls back to the `main` field, then tries `index.js`, `server.js`, or `app.js` in that order.

### Optional Fields

#### `scripts.build`

If present, the manager runs `npm run build` after `npm install` and before starting the app. Use this for:
- TypeScript compilation
- Frontend asset bundling
- Any pre-run build step

```json
{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}
```

#### `engines.node`

If present, the manager validates that the server's Node.js version satisfies this range **before** deploying. If it doesn't match, deployment is rejected with a clear error.

```json
{
  "engines": {
    "node": ">=18"
  }
}
```

Use standard semver ranges: `>=18`, `^20.0.0`, `18 || 20`, etc.

#### `main`

Fallback entry point if no `start` script exists. PM2 runs this file directly.

```json
{
  "main": "server.js"
}
```

---

## PORT Handling

**This is the single most important rule**: your app must listen on the port provided by the `PORT` environment variable.

### The Pattern

```js
const PORT = process.env.PORT || 3000;
```

- When deployed, the manager assigns a unique port and passes it as `PORT`
- When developing locally, `process.env.PORT` is undefined, so the fallback (`3000`) is used
- **Never hardcode a port** -- this will conflict with other apps on the server

### For Express Apps

```js
const express = require('express');
const app = express();

const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.send('Hello World');
});

app.listen(PORT, () => {
  console.log(`Listening on port ${PORT}`);
});
```

### For Fastify Apps

```js
const fastify = require('fastify')();

const PORT = process.env.PORT || 3000;

fastify.get('/', async () => ({ hello: 'world' }));

fastify.listen({ port: PORT, host: '0.0.0.0' }, (err) => {
  if (err) throw err;
  console.log(`Listening on port ${PORT}`);
});
```

### Testing PORT Locally

Verify your app respects the PORT variable before deploying:

```bash
# Default port (uses fallback)
npm start
# App should be at http://localhost:3000

# Custom port (simulates deployment)
PORT=4567 npm start
# App should be at http://localhost:4567
```

---

## Local Development

### Standard Workflow

```bash
# Install dependencies
npm install

# Run in development
npm start

# Or with a file watcher (if using nodemon)
npm run dev
```

### Recommended dev script

Add a `dev` script for development convenience (this is NOT used by the manager):

```json
{
  "scripts": {
    "dev": "node --watch index.js",
    "start": "node index.js"
  }
}
```

### Environment Variables Locally

Create a `.env` file for local development (make sure it's in `.gitignore` and excluded from your zip):

```
PORT=3000
DATABASE_URL=sqlite://local.db
API_KEY=dev-key-123
```

Use `dotenv` or similar to load it:

```js
require('dotenv').config();
```

When deployed, the manager passes your custom env vars directly to the process -- no `.env` file needed on the server.

---

## Build Step

If your app requires a build step (TypeScript, bundling, etc.), add a `build` script.

### Deployment Pipeline

The manager runs these steps in order:

1. `npm install --production` -- installs dependencies
2. `npm run build` -- **only if** a `build` script exists
3. `npm start` -- starts the app

### TypeScript Example

```json
{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "ts-node src/index.ts"
  },
  "devDependencies": {
    "typescript": "^5.0.0"
  }
}
```

**Important**: Since `npm install --production` skips devDependencies, your `build` script must work without them OR list build tools as regular `dependencies`. A common pattern:

```json
{
  "dependencies": {
    "typescript": "^5.0.0"
  }
}
```

Alternatively, restructure to avoid needing a build step in production (e.g., commit compiled output).

---

## Environment Variables

Apps often need configuration (API keys, database URLs, etc.). The manager supports per-app environment variables.

### Declaring What Your App Needs

Document required env vars in your README or package.json:

```json
{
  "appsManager": {
    "envVars": {
      "DATABASE_URL": "Connection string for the database",
      "API_KEY": "Third-party API key",
      "LOG_LEVEL": "debug, info, warn, error (default: info)"
    }
  }
}
```

This is informational only -- the manager doesn't read this field, but it helps humans and agents know what to configure when deploying.

### Reading Env Vars in Code

```js
const config = {
  databaseUrl: process.env.DATABASE_URL || 'sqlite://data.db',
  apiKey: process.env.API_KEY, // required, no fallback
  logLevel: process.env.LOG_LEVEL || 'info',
};

if (!config.apiKey) {
  console.error('ERROR: API_KEY environment variable is required');
  process.exit(1);
}
```

### Setting Env Vars at Deploy Time

When deploying through the manager UI, you can add key-value pairs in the "Environment Variables" section. These are passed to PM2 and available as `process.env.KEY` in your app.

---

## Zip Packaging

### What to Include

- All source files (`*.js`, `*.ts`, `src/`, etc.)
- `package.json` (required)
- `package-lock.json` (recommended, ensures reproducible installs)
- Static assets, config files, templates
- `tsconfig.json` if using TypeScript

### What to Exclude

- `node_modules/` -- the manager runs `npm install`
- `.env` -- secrets should be set via the manager UI
- `.git/` -- not needed for deployment
- Build artifacts that will be regenerated (e.g., `dist/` if you have a build step)
- OS files (`.DS_Store`, `Thumbs.db`)

### Creating the Zip

From your project root:

```bash
# Simple: zip everything except excluded dirs
zip -r my-app.zip . -x "node_modules/*" ".git/*" ".env" ".DS_Store"
```

Or create a `.zipignore`-style script:

```bash
#!/bin/bash
APP_NAME=$(node -e "console.log(require('./package.json').name)")
zip -r "${APP_NAME}.zip" . \
  -x "node_modules/*" \
  -x ".git/*" \
  -x ".env" \
  -x ".DS_Store" \
  -x "*.log"
```

### Flat Structure Requirement

The zip must have `package.json` at the root level. Both of these structures work:

**Flat (preferred)**:
```
my-app.zip
├── package.json
├── index.js
└── src/
    └── ...
```

**Single root folder (also supported)**:
```
my-app.zip
└── my-app/
    ├── package.json
    ├── index.js
    └── src/
        └── ...
```

The manager auto-detects a single root folder and flattens it. However, this will **not** work:

```
my-app.zip
├── frontend/
│   └── package.json
└── backend/
    └── package.json
```

Each zip must contain exactly one app.

---

## Graceful Shutdown

When the manager stops or restarts your app, PM2 sends a `SIGTERM` signal. Handle it to close connections cleanly:

```js
const server = app.listen(PORT);

process.on('SIGTERM', () => {
  console.log('SIGTERM received, shutting down gracefully...');
  server.close(() => {
    // Close database connections, flush buffers, etc.
    process.exit(0);
  });
});
```

If your app doesn't exit within 1.6 seconds of receiving `SIGTERM`, PM2 sends `SIGKILL` (force kill).

---

## Logging

Use `console.log()` for stdout and `console.error()` for stderr. PM2 captures both automatically and the manager UI displays them.

```js
console.log('Server started');           // -> stdout
console.log('Request:', req.method, req.url);
console.error('Failed to connect to DB:', err.message);  // -> stderr
```

### Do NOT

- Write logs to files -- PM2 already handles log persistence
- Use custom log transports that write to disk
- Use `process.stdout.write()` for structured logs (it works, but `console.log` is simpler)

### Structured Logging

If you want structured logs, JSON is fine -- PM2 captures it as-is:

```js
console.log(JSON.stringify({ level: 'info', msg: 'request', method: 'GET', path: '/' }));
```

---

## Common Pitfalls

A checklist of things that break deployment:

1. **Hardcoded port** -- Always use `process.env.PORT || <fallback>`
2. **Missing `start` script** -- The manager needs to know how to run your app
3. **`node_modules` in the zip** -- Wastes upload time and may cause platform issues; let the server run `npm install`
4. **Nested folder structure** -- `package.json` must be at the root (or inside a single root folder)
5. **Build tools in devDependencies** -- `npm install --production` skips devDependencies; move build tools to `dependencies` if you have a `build` script
6. **Listening on `0.0.0.0` vs `localhost`** -- Both work, but `0.0.0.0` is safer for containerized environments
7. **Relative file paths** -- Use `__dirname` or `path.join` instead of relative paths that assume a specific working directory
8. **Missing `engines.node`** -- Not strictly required but recommended; avoids deploying to an incompatible Node.js version
9. **Large zip files** -- Max upload size is 100 MB; keep your zip lean
10. **Forgetting `.env` exclusion** -- Never include secrets in the zip; set env vars through the manager UI

---

## Full Example: Express API

A complete, production-ready example:

**package.json**
```json
{
  "name": "todo-api",
  "version": "1.0.0",
  "scripts": {
    "start": "node index.js",
    "dev": "node --watch index.js"
  },
  "engines": {
    "node": ">=18"
  },
  "dependencies": {
    "express": "^4.18.0",
    "cors": "^2.8.0"
  }
}
```

**index.js**
```js
const express = require('express');
const cors = require('cors');

const app = express();
const PORT = process.env.PORT || 3000;

app.use(cors());
app.use(express.json());

let todos = [];
let nextId = 1;

app.get('/todos', (req, res) => {
  res.json(todos);
});

app.post('/todos', (req, res) => {
  const todo = { id: nextId++, text: req.body.text, done: false };
  todos.push(todo);
  res.status(201).json(todo);
});

app.patch('/todos/:id', (req, res) => {
  const todo = todos.find(t => t.id === parseInt(req.params.id));
  if (!todo) return res.status(404).json({ error: 'Not found' });
  Object.assign(todo, req.body);
  res.json(todo);
});

app.delete('/todos/:id', (req, res) => {
  todos = todos.filter(t => t.id !== parseInt(req.params.id));
  res.status(204).end();
});

const server = app.listen(PORT, () => {
  console.log(`Todo API running on port ${PORT}`);
});

process.on('SIGTERM', () => {
  console.log('Shutting down...');
  server.close(() => process.exit(0));
});
```

**Deploy it**:
```bash
cd todo-api
zip -r todo-api.zip . -x "node_modules/*" ".git/*" ".env"
# Upload todo-api.zip through the manager UI with name "todo-api"
# Access at https://todo-api.your-domain.com
```
