How to Create and Deploy New API

Complete guide for creating and deploying a new NestJS API following GoGrab patterns

How to Create and Deploy New API

This guide documents the complete process for creating and deploying a new NestJS API in the GoGrab monorepo, following established patterns from backend-api, backend-ai-api, and backend-docs-api.

Prerequisites


Phase 1: Create the NestJS Application

Step 1.1: Create Project Structure

Create the application folder structure:

apis/backend-{name}-api/
├── src/
│   ├── app/
│   │   ├── constants.ts           # ⚠️ CRITICAL: Avoid circular deps
│   │   ├── app.module.ts
│   │   ├── app.controller.ts
│   │   ├── app.service.ts
│   │   └── {feature}/             # Feature modules
│   │       ├── {feature}.module.ts
│   │       ├── {feature}.controller.ts
│   │       ├── {feature}.service.ts
│   │       └── schemas/
│   │           └── {model}.schema.ts
│   ├── main.ts
│   └── assets/
├── project.json
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
├── webpack.config.js
└── jest.config.ts

Step 1.2: Create project.json

{
  "name": "backend-{name}-api",
  "$schema": "../../node_modules/nx/schemas/project-schema.json",
  "sourceRoot": "apis/backend-{name}-api/src",
  "projectType": "application",
  "tags": ["backend", "nestjs", "{name}"],
  "targets": {
    "build": {
      "executor": "@nx/webpack:webpack",
      "outputs": ["{options.outputPath}"],
      "dependsOn": ["^build"],
      "options": {
        "target": "node",
        "compiler": "tsc",
        "outputPath": "dist/apis/backend-{name}-api",
        "main": "apis/backend-{name}-api/src/main.ts",
        "tsConfig": "apis/backend-{name}-api/tsconfig.app.json",
        "assets": ["apis/backend-{name}-api/src/assets"],
        "isolatedConfig": true,
        "webpackConfig": "apis/backend-{name}-api/webpack.config.js",
        "generatePackageJson": true
      },
      "configurations": {
        "development": {
          "sourceMap": true,
          "optimization": false
        },
        "production": {
          "sourceMap": false,
          "optimization": true,
          "extractLicenses": true,
          "inspect": false
        }
      },
      "defaultConfiguration": "production"
    },
    "serve": {
      "continuous": true,
      "executor": "@nx/js:node",
      "defaultConfiguration": "development",
      "dependsOn": ["build"],
      "options": {
        "buildTarget": "backend-{name}-api:build",
        "runBuildTargetDependencies": false
      },
      "configurations": {
        "development": {
          "buildTarget": "backend-{name}-api:build:development"
        },
        "production": {
          "buildTarget": "backend-{name}-api:build:production"
        }
      }
    }
  }
}

Step 1.3: Create webpack.config.js

const { composePlugins, withNx } = require('@nx/webpack');

module.exports = composePlugins(withNx(), (config) => {
  return config;
});

Step 1.4: Create tsconfig.app.json

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "../../dist/out-tsc",
    "module": "commonjs",
    "types": ["node"],
    "emitDecoratorMetadata": true,
    "target": "es2021"
  },
  "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
  "include": ["src/**/*.ts"]
}

Phase 2: Set Up Core Modules

Step 2.1: Create constants.ts (CRITICAL!)

⚠️ This file prevents circular dependencies. Always define shared constants here, NOT in app.module.ts.

// apis/backend-{name}-api/src/app/constants.ts

/**
 * Backend {Name} API Constants
 * 
 * ⚠️ CRITICAL: Keep constants here to avoid circular dependencies.
 * Feature modules import from here, NOT from app.module.ts
 */

/**
 * MongoDB connection name for this API's database
 */
export const {NAME}_CONNECTION_NAME = '{name}-database';

Step 2.2: Create main.ts

🎨 Choose a Distinctive Color!

Each API should have a unique color so you can easily distinguish them in the terminal when running multiple backends simultaneously.

API Color ANSI Code Preview
backend-api Blue \x1b[34m 🔵
backend-ai-api Cyan \x1b[36m 🩵
backend-docs-api Orange \x1b[38;5;208m 🟠
(your new API) Magenta \x1b[35m 🟣
(your new API) Green \x1b[32m 🟢
(your new API) Yellow \x1b[33m 🟡
import { ConsoleLogger, Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app/app.module';
import {
  createCorsConfig,
  logCorsConfig,
  createDefaultValidationPipe,
} from '@libs-api/backend-core';

class {Name}ApiLogger extends ConsoleLogger {
  // 🎨 Pick a unique color from the table above!
  private readonly color = '\x1b[38;5;208m';  // Orange (or use \x1b[35m for magenta, etc.)
  private readonly reset = '\x1b[0m';

  protected formatPid(pid: number): string {
    return `${this.color}[{NAME}-API]${this.reset} ${pid}  - `;
  }
}

async function bootstrap() {
  const isRailway = process.env.RAILWAY_ENVIRONMENT === 'true' || 
                    process.env.RAILWAY_STATIC_URL !== undefined ||
                    process.env.RAILWAY_SERVICE_NAME !== undefined;
  
  const isProduction = process.env.NODE_ENV === 'production' || isRailway;
  
  const app = await NestFactory.create(AppModule, {
    logger: isProduction
      ? new {Name}ApiLogger('{NAME}-API', { logLevels: ['error', 'warn'] })
      : new {Name}ApiLogger('{NAME}-API', { logLevels: ['error', 'warn', 'log'] }),
  });

  // Global prefix - MUST be unique across all APIs
  const globalPrefix = '{name}-api';  // e.g., 'docs-api', 'ai-api'
  app.setGlobalPrefix(globalPrefix);

  const logger = new Logger('Bootstrap');
  app.enableCors(
    createCorsConfig({
      debug: false,
      logger,
      additionalOrigins: [
        'http://localhost:3000',  // backend-api
        'http://localhost:3001',  // backend-ai-api
        'https://gograb.space',   // production
      ],
    })
  );

  app.useGlobalPipes(createDefaultValidationPipe());

  // Port configuration
  const port = isRailway
    ? (process.env.PORT || 8080)           // Railway: dynamic PORT
    : (process.env.{NAME}_API_PORT || 3002); // Local: custom port
  
  await app.listen(port, '0.0.0.0');

  console.log(`\n📚 GoGrab Backend {Name} API READY on port ${port}\n`);
}

bootstrap().catch(err => {
  console.error('❌ Failed to start backend-{name}-api:', err);
  process.exit(1);
});

Step 2.3: Create app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { ThrottlerModule } from '@nestjs/throttler';
import { AppController } from './app.controller';
import { AppService } from './app.service';

// ✅ Shared libraries (DRY)
import {
  createMongooseConfig,
  AI_THROTTLE_CONFIG,
} from '@libs-api/backend-core';
import { AuthCoreModule } from '@libs-api/auth-core';

// ✅ Import from constants.ts, NOT define here
import { {NAME}_CONNECTION_NAME } from './constants';

// Feature Modules
import { {Feature}Module } from './{feature}/{feature}.module';

@Module({
  imports: [
    // Configuration - Global
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: ['.env.local', '.env'],
    }),

    // Shared Authentication (same JWT across all APIs)
    AuthCoreModule.forRoot({
      jwtSecretKey: 'JWT_SECRET',
      systemTokenKey: 'SYSTEM_TOKEN',
      jwtExpiration: '7d',
    }),

    // MongoDB Connection (use appropriate cluster)
    MongooseModule.forRootAsync({
      connectionName: {NAME}_CONNECTION_NAME,  // From constants.ts!
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) =>
        createMongooseConfig(configService, {
          dbName: '{name}-database',
          appName: 'backend-{name}-api',
          connectionStringKey: 'MONGODB_CONNECTION_STRING', // Or custom key
          maxPoolSize: 20,
          minPoolSize: 5,
        }),
      inject: [ConfigService],
    }),

    // Rate Limiting
    ThrottlerModule.forRoot(AI_THROTTLE_CONFIG),

    // Feature Modules
    {Feature}Module,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Step 2.4: Create Health Check Controller

// apis/backend-{name}-api/src/app/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { Public } from '@libs-api/auth-core';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Public()
  @Get('health')
  getHealth() {
    return this.appService.getHealth();
  }

  @Public()
  @Get()
  getData() {
    return this.appService.getData();
  }
}

// apis/backend-{name}-api/src/app/app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHealth() {
    return {
      status: 'ok',
      timestamp: new Date().toISOString(),
      service: 'backend-{name}-api',
      database: '{name}-database',
    };
  }

  getData() {
    return {
      message: 'GoGrab Backend {Name} API',
      version: '1.0.0',
    };
  }
}

Phase 3: Create Feature Modules

Step 3.1: Feature Module Pattern

// apis/backend-{name}-api/src/app/{feature}/{feature}.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { {Feature}Controller } from './{feature}.controller';
import { {Feature}Service } from './{feature}.service';
import { {Model}, {Model}Schema } from './schemas/{model}.schema';

// ⚠️ Import from constants.ts, NOT app.module.ts!
import { {NAME}_CONNECTION_NAME } from '../constants';

@Module({
  imports: [
    MongooseModule.forFeature(
      [{ name: {Model}.name, schema: {Model}Schema }],
      {NAME}_CONNECTION_NAME,  // Must match the connection name!
    ),
  ],
  controllers: [{Feature}Controller],
  providers: [{Feature}Service],
  exports: [{Feature}Service],
})
export class {Feature}Module {}

Step 3.2: Service with Injected Models

// apis/backend-{name}-api/src/app/{feature}/{feature}.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { {Model}, {Model}Document } from './schemas/{model}.schema';

// ⚠️ Import from constants.ts!
import { {NAME}_CONNECTION_NAME } from '../constants';

@Injectable()
export class {Feature}Service {
  constructor(
    @InjectModel({Model}.name, {NAME}_CONNECTION_NAME)
    private {model}Model: Model<{Model}Document>,
  ) {}

  // Service methods...
}

Phase 4: Route Syntax (CRITICAL!)

⚠️ NestJS 11 / path-to-regexp Compatibility

The new version of path-to-regexp requires different syntax for wildcard routes:

// ❌ OLD SYNTAX - WILL CRASH THE APP
@Get('files/:path(*)')  

// ✅ NEW SYNTAX - Use this
@Get('files/*path')

Common patterns:

// Single parameter
@Get(':id')           // /api/items/123

// Optional wildcard path (for file paths, nested routes)
@Get('*path')         // /api/files/docs/english/guide.md

// Multiple parameters
@Get(':userId/items/:itemId')

Phase 5: Local Testing

Step 5.1: Add Port to .env.local

# .env.local
{NAME}_API_PORT=300X  # Choose unique port (3002, 3003, etc.)

Step 5.2: Build and Test

# Build
nx build backend-{name}-api --configuration=production

# Run locally
node dist/apis/backend-{name}-api/main.js

# Test health endpoint
curl http://localhost:300X/{name}-api/health

Expected response:

{"status":"ok","timestamp":"...","service":"backend-{name}-api","database":"{name}-database"}

Phase 6: Railway Deployment

Step 6.1: Create Railway Project

  1. Go to https://railway.app
  2. Create new project: "Backend {Name} Api"
  3. Add a new Empty Service named backend-{name}-api
  4. Go to Settings → Tokens → Create Project Token
  5. Save token to .env.local:
# .env.local
RAILWAY_{NAME}_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
RAILWAY_PROJECTID_{NAME}=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Step 6.2: Set Environment Variables in Railway

Required variables (copy from existing APIs where applicable):

Variable Value Notes
NODE_ENV production
JWT_SECRET (same as backend-api) Must match all APIs
SYSTEM_TOKEN (same as backend-api) Must match all APIs
MONGODB_CONNECTION_STRING mongodb+srv://... Your cluster
ALLOWED_ORIGINS https://gograb.space,... CORS origins

Step 6.3: Create Dockerfile

# deployment/Dockerfile.railway.{name}

# Railway Backend {Name} API Deployment Dockerfile
FROM node:22-alpine

# Install curl for health checks
RUN apk add --no-cache wget curl ca-certificates && \
    update-ca-certificates

WORKDIR /app

# Copy package files
COPY package*.json ./
COPY yarn.lock ./

# Configure npm/yarn for Railway
ENV NPM_CONFIG_FETCH_RETRY_MINTIMEOUT=10000
ENV NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT=120000
ENV NPM_CONFIG_FETCH_RETRIES=5
ENV NPM_CONFIG_FETCH_TIMEOUT=300000
ENV NPM_CONFIG_NETWORK_TIMEOUT=600000
ENV YARN_NETWORK_TIMEOUT=600000

# Install dependencies with retry
RUN set -e; \
    yarn cache clean; \
    rm -f package-lock.json; \
    for i in 1 2 3 4 5; do \
        echo "Attempt $i: Installing dependencies..."; \
        if yarn install --network-timeout 600000 --no-optional; then \
            echo "Dependencies installed successfully"; \
            break; \
        else \
            echo "Attempt $i failed, retrying in 30 seconds..."; \
            if [ $i -eq 5 ]; then \
                echo "All attempts failed"; \
                exit 1; \
            fi; \
            sleep 30; \
            yarn cache clean; \
            rm -rf node_modules; \
        fi; \
    done

COPY . .

# Build
RUN echo "📚 Building backend-{name}-api..." && \
    npx nx build backend-{name}-api --skip-nx-cache --configuration=production

# Verify build
RUN if [ ! -f "dist/apis/backend-{name}-api/main.js" ]; then \
        echo "❌ Build failed - main.js not found"; \
        exit 1; \
    else \
        echo "✅ Build successful"; \
    fi

# Cleanup
RUN rm -rf node_modules/.cache && yarn cache clean

# Railway detection
ENV RAILWAY_ENVIRONMENT=true

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:${PORT:-8080}/{name}-api/health || exit 1

CMD ["node", "dist/apis/backend-{name}-api/main.js"]

Step 6.4: Create Railway Config

# deployment/railway.{name}.toml

[build]
builder = "DOCKERFILE"
dockerfilePath = "deployment/Dockerfile.railway.{name}"
dockerContext = "."

[deploy]
startCommand = "node dist/apis/backend-{name}-api/main.js"
healthcheckPath = "/{name}-api/health"
healthcheckTimeout = 300
restartPolicyType = "on_failure"
restartPolicyMaxRetries = 10

Step 6.5: Create Deploy Script

#!/bin/bash
# deployment/deploy-{name}-api.sh

set -e

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"

# Load tokens
if [ -f "$PROJECT_ROOT/.env.local" ]; then
    echo -e "${BLUE}🔐 Loading tokens from .env.local${NC}"
    export $(grep -v '^#' "$PROJECT_ROOT/.env.local" | grep '=' | xargs)
fi

# Railway CLI
RAILWAY_CLI="${RAILWAY_CLI:-railway}"
if ! command -v $RAILWAY_CLI &> /dev/null; then
    RAILWAY_CLI="/Users/fguijarro/.nvm/versions/node/v22.15.0/lib/node_modules/@railway/cli/bin/railway"
fi

# Validate token
if [ -z "$RAILWAY_{NAME}_TOKEN" ]; then
    echo -e "${RED}❌ RAILWAY_{NAME}_TOKEN not set!${NC}"
    exit 1
fi
export RAILWAY_TOKEN="$RAILWAY_{NAME}_TOKEN"

echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║        🚀 Backend {Name} API Railway Deployment            ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"

# Backup and swap railway.toml
if [ -f railway.toml ]; then
    cp railway.toml railway.toml.backup
fi

cp deployment/railway.{name}.toml railway.toml

# Deploy with retry
MAX_ATTEMPTS=3
ATTEMPT=1

while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
    echo -e "${BLUE}   Attempt $ATTEMPT of $MAX_ATTEMPTS...${NC}"
    
    if $RAILWAY_CLI up --service backend-{name}-api --detach; then
        echo -e "${GREEN}   ✅ Deployment triggered!${NC}"
        break
    else
        echo -e "${YELLOW}   ⚠️ Attempt $ATTEMPT failed${NC}"
        
        if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
            echo -e "${RED}   ❌ All attempts failed${NC}"
        else
            echo -e "${BLUE}   Waiting 30 seconds...${NC}"
            sleep 30
        fi
        ATTEMPT=$((ATTEMPT + 1))
    fi
done

# Restore railway.toml
if [ -f railway.toml.backup ]; then
    mv railway.toml.backup railway.toml
fi

echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║        ✅ Deployment Triggered!                            ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"

Step 6.6: Deploy

Option A: Use the unified deploy script (Recommended)

# Deploy any API with the unified script
./deployment/deploy-api.sh backend-{name}-api

# Or use yarn shortcuts:
yarn railway:deploy:api backend-{name}-api

Option B: Use the API-specific script

# Make executable
chmod +x deployment/deploy-{name}-api.sh

# Deploy
./deployment/deploy-{name}-api.sh

How the Unified Deploy Script Works

The deploy-api.sh script:

  1. Takes the API name as a parameter
  2. Looks up the configuration (token, prefix, Dockerfile)
  3. Generates railway.toml dynamically with correct settings
  4. Deploys using Railway CLI
  5. Cleans up the temporary railway.toml

This avoids the need to maintain separate deploy scripts and prevents the "wrong Dockerfile" issue.


Common Issues & Solutions

Issue 1: Circular Dependency Error

Error: Cannot access '{NAME}_CONNECTION_NAME' before initialization

Solution: Move constants to constants.ts, NOT app.module.ts:

// ❌ WRONG - in app.module.ts
export const CONNECTION_NAME = 'db';

// ✅ CORRECT - in constants.ts
export const CONNECTION_NAME = 'db';

Issue 2: Route Syntax Error

Unsupported route path: "/:path(*)". Use named parameters instead.

Solution: Use new path-to-regexp syntax:

// ❌ OLD
@Get(':path(*)')

// ✅ NEW
@Get('*path')

Issue 3: MongoDB Connection Not Found

Nest can't resolve dependencies of the Model (?). 
Please make sure that the argument "DatabaseConnection" is available.

Solution: Ensure:

  1. connectionName in MongooseModule.forRootAsync matches
  2. @InjectModel(Model.name, CONNECTION_NAME) uses same name
  3. Import CONNECTION_NAME from constants.ts, not app.module.ts

Issue 4: Health Check Fails

1/1 replicas never became healthy!

Debugging:

  1. Check Railway logs for startup errors
  2. Verify healthcheckPath matches your actual route
  3. Check if all env vars are set (especially MONGODB_CONNECTION_STRING)

Issue 5: CLI Upload Timeout

operation timed out

Solutions:

Issue 6: WRONG API Deployed (CRITICAL!)

# You expected to see:
📚 GoGrab Backend Docs API READY on port 8080

# But logs show:
🤖 GoGrab Backend AI API READY on port 8080

Cause: Railway dashboard has the wrong Dockerfile path configured!

Solution:

  1. Go to Railway Dashboard → Your Service → Settings
  2. Check Build section → Dockerfile Path
  3. Verify it points to the CORRECT Dockerfile:
Service Correct Dockerfile Path
backend-api deployment/Dockerfile.railway.backend
backend-ai-api deployment/Dockerfile.railway.backend-ai
backend-docs-api deployment/Dockerfile.railway.docs-api
  1. Also verify Start Command and Health Check Path match the API
  2. Redeploy after fixing

⚠️ ALWAYS verify logs show the correct API name after deployment!


🚨 CRITICAL: Verify Railway Dashboard Settings

After creating a new Railway service, ALWAYS manually verify these settings in the Railway Dashboard:

Build Settings (Settings → Build)

Setting Must Be
Builder Dockerfile
Dockerfile Path deployment/Dockerfile.railway.{name}
Watch Paths / or default

Deploy Settings (Settings → Deploy)

Setting Must Be
Start Command node dist/apis/backend-{name}-api/main.js
Health Check Path /{name}-api/health
Health Check Timeout 300 seconds

Post-Deployment Verification

ALWAYS check Railway logs after deployment to confirm the correct API started:

# ✅ CORRECT - You should see:
[{NAME}-API] 1  - 12/22/2025, 3:55:09 PM ...
📚 GoGrab Backend {Name} API READY on port 8080

# ❌ WRONG - If you see a DIFFERENT API name:
[AI-API] 1  - 12/22/2025, 3:55:09 PM ...
🤖 GoGrab Backend AI API READY on port 8080  ← WRONG API!

If logs show the wrong API, the Dockerfile path in Railway is incorrect!


Checklist

Pre-Deployment

Railway Setup

Deployment Files

⚠️ Railway Dashboard Verification (CRITICAL!)

Post-Deployment


Reference: Existing APIs

API Port Prefix MongoDB Log Color
backend-api 3000 /api gograb-api cluster 🔵 Blue
backend-ai-api 3001 /ai-api gograb-api cluster 🩵 Cyan
backend-docs-api 3002 /docs-api test-modules cluster 🟠 Orange

Production URLs (Railway)

API Railway Domain Health Check
backend-api backend-api-production-97c0.up.railway.app /api/health
backend-ai-api backend-ai-api-production.up.railway.app /ai-api/health
backend-docs-api backend-docs-api-production.up.railway.app /docs-api/health

Related Documentation