This guide walks through the process of adding a new service to the Oshun monorepo.
Overview#
Adding a new service involves:
- Creating the application project
- Setting up the project configuration
- Implementing the service
- Adding API contracts
- Setting up tests
- 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:
text
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#
- Use shared libraries - Don't duplicate code that exists in
libs/shared/ - Follow naming conventions - Use the established patterns
- Add health checks - Every service should have health endpoints
- Document APIs - Keep OpenAPI specs up to date
- Write tests - Aim for good test coverage
- Handle errors properly - Use the shared error handling utilities
- Log appropriately - Use structured logging from
@oshun/logging