# Adding a New Service

This guide walks through the process of adding a new service to the Oshun
monorepo.

## Overview

Adding a new service involves:

1. Creating the application project
2. Setting up the project configuration
3. Implementing the service
4. Adding API contracts
5. Setting up tests
6. Configuring deployment

## Step 1: Create the Application

### Using Nx Generator

```bash
# Create a new Node.js application
nx g @nx/node:application my-service --directory=apps/{domain}/my-service
```

Replace `{domain}` with the appropriate domain (e.g., iris, lilith, yemaya,
isis, sophia, hathor, bellona, tara, veritas, psyche, nyx, aja, or aphrodite).

### Manual Creation

Create the directory structure:

```
apps/{domain}/my-service/
├── src/
│   ├── app.ts
│   ├── main.ts
│   └── routes/
├── project.json
├── package.json
├── tsconfig.json
├── Dockerfile
└── .env.example
```

## Step 2: Configure project.json

Create `apps/{domain}/my-service/project.json`:

```json
{
  "name": "@{domain}/my-service",
  "$schema": "../../../node_modules/nx/schemas/project-schema.json",
  "sourceRoot": "apps/{domain}/my-service/src",
  "projectType": "application",
  "tags": ["scope:{domain}", "type:app", "platform:node"],
  "targets": {
    "build": {
      "executor": "nx:run-commands",
      "outputs": ["{projectRoot}/dist"],
      "options": {
        "cwd": "apps/{domain}/my-service",
        "command": "tsc"
      }
    },
    "dev": {
      "executor": "nx:run-commands",
      "options": {
        "cwd": "apps/{domain}/my-service",
        "command": "tsx watch src/main.ts"
      }
    },
    "serve": {
      "executor": "nx:run-commands",
      "options": {
        "cwd": "apps/{domain}/my-service",
        "command": "node dist/main.js"
      }
    },
    "test": {
      "executor": "@nx/vite:test",
      "options": {
        "config": "apps/{domain}/my-service/vitest.config.ts"
      }
    },
    "lint": {
      "executor": "@nx/eslint:lint",
      "outputs": ["{options.outputFile}"]
    },
    "typecheck": {
      "executor": "nx:run-commands",
      "options": {
        "cwd": "apps/{domain}/my-service",
        "command": "tsc --noEmit"
      }
    }
  }
}
```

## Step 3: Create package.json

```json
{
  "name": "@{domain}/my-service",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/main.ts",
    "build": "tsc",
    "start": "node dist/main.js",
    "test": "vitest run",
    "lint": "eslint src --ext ts",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@hono/node-server": "catalog:",
    "hono": "catalog:",
    "@oshun/config": "workspace:*",
    "@oshun/logging": "workspace:*",
    "@oshun/errors": "workspace:*",
    "@oshun/health": "workspace:*"
  },
  "devDependencies": {
    "@types/node": "catalog:",
    "tsx": "catalog:",
    "typescript": "catalog:",
    "vitest": "catalog:"
  }
}
```

## Step 4: Implement the Service

### Entry Point (src/main.ts)

```typescript
import { serve } from '@hono/node-server';
import { createApp } from './app';
import { createLogger } from '@oshun/logging';

const logger = createLogger({ service: 'my-service' });

const app = createApp();

const port = parseInt(process.env.PORT || '3000', 10);

serve(
  {
    fetch: app.fetch,
    port,
  },
  (info) => {
    logger.info(`Server running on http://localhost:${info.port}`);
  }
);
```

### Application (src/app.ts)

```typescript
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { healthRoutes } from './routes/health';
import { apiRoutes } from './routes/api';
import { errorMiddleware } from '@oshun/errors';

export function createApp() {
  const app = new Hono();

  // Middleware
  app.use('*', cors());
  app.use('*', errorMiddleware());

  // Routes
  app.route('/health', healthRoutes);
  app.route('/api', apiRoutes);

  return app;
}
```

### Health Routes (src/routes/health.ts)

```typescript
import { Hono } from 'hono';

export const healthRoutes = new Hono();

healthRoutes.get('/', (c) => c.json({ status: 'ok' }));
healthRoutes.get('/ready', (c) => c.json({ status: 'ready' }));
healthRoutes.get('/live', (c) => c.json({ status: 'live' }));
```

## Step 5: Add API Contract

Create OpenAPI spec at `libs/openapi/specs/{domain}/my-service.yaml`:

```yaml
openapi: 3.1.0
info:
  title: My Service API
  version: 1.0.0
  description: API for my service

servers:
  - url: http://localhost:3000
    description: Development server

paths:
  /health:
    get:
      summary: Health check
      operationId: getHealth
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok

  /api/items:
    get:
      summary: List items
      operationId: listItems
      responses:
        '200':
          description: List of items
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Item'

components:
  schemas:
    Item:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: string
        name:
          type: string
```

## Step 6: Add Tests

Create `apps/{domain}/my-service/vitest.config.ts`:

```typescript
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['src/**/*.{test,spec}.ts'],
  },
});
```

Create a test file `src/app.test.ts`:

```typescript
import { describe, it, expect } from 'vitest';
import { createApp } from './app';

describe('App', () => {
  const app = createApp();

  describe('GET /health', () => {
    it('should return ok status', async () => {
      const res = await app.request('/health');
      expect(res.status).toBe(200);

      const body = await res.json();
      expect(body.status).toBe('ok');
    });
  });
});
```

## Step 7: Create Dockerfile

```dockerfile
FROM node:20-alpine AS builder

WORKDIR /app

# Copy workspace files
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY libs/shared/ ./libs/shared/
COPY apps/{domain}/my-service/ ./apps/{domain}/my-service/

# Install dependencies
RUN corepack enable && pnpm install --frozen-lockfile

# Build
RUN pnpm --filter @{domain}/my-service build

FROM node:20-alpine AS runner

WORKDIR /app

COPY --from=builder /app/apps/{domain}/my-service/dist ./dist
COPY --from=builder /app/apps/{domain}/my-service/package.json ./

RUN corepack enable && pnpm install --prod --frozen-lockfile

ENV NODE_ENV=production
EXPOSE 3000

CMD ["node", "dist/main.js"]
```

## Step 8: Environment Variables

Create `.env.example`:

```bash
# Server
PORT=3000
NODE_ENV=development

# Logging
LOG_LEVEL=debug

# Database (if needed)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb

# Add other environment variables as needed
```

## Step 9: Register in CI

Add the service to `.github/workflows/ci.yml` if needed:

```yaml
jobs:
  build:
    # ... existing config
    steps:
      - name: Build my-service
        run: nx build @{domain}/my-service
```

## Checklist

Before submitting your PR, ensure:

- [ ] Project compiles without errors
- [ ] All tests pass
- [ ] OpenAPI spec is valid
- [ ] Dockerfile builds successfully
- [ ] Environment variables are documented
- [ ] README is updated (if needed)

## Best Practices

1. **Use shared libraries** - Don't duplicate code that exists in `libs/shared/`
2. **Follow naming conventions** - Use the established patterns
3. **Add health checks** - Every service should have health endpoints
4. **Document APIs** - Keep OpenAPI specs up to date
5. **Write tests** - Aim for good test coverage
6. **Handle errors properly** - Use the shared error handling utilities
7. **Log appropriately** - Use structured logging from `@oshun/logging`
