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
- Node.js 22+
- NX workspace knowledge
- Railway account with project access
- MongoDB Atlas cluster access
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
- Go to https://railway.app
- Create new project: "Backend {Name} Api"
- Add a new Empty Service named
backend-{name}-api - Go to Settings → Tokens → Create Project Token
- 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:
- Takes the API name as a parameter
- Looks up the configuration (token, prefix, Dockerfile)
- Generates
railway.tomldynamically with correct settings - Deploys using Railway CLI
- 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:
connectionNameinMongooseModule.forRootAsyncmatches@InjectModel(Model.name, CONNECTION_NAME)uses same name- Import
CONNECTION_NAMEfromconstants.ts, notapp.module.ts
Issue 4: Health Check Fails
1/1 replicas never became healthy!
Debugging:
- Check Railway logs for startup errors
- Verify
healthcheckPathmatches your actual route - Check if all env vars are set (especially
MONGODB_CONNECTION_STRING)
Issue 5: CLI Upload Timeout
operation timed out
Solutions:
- Use
--detachflag (deployment continues in background) - Try from native terminal (not Cursor)
- Check if upload triggered:
railway status --json
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:
- Go to Railway Dashboard → Your Service → Settings
- Check Build section → Dockerfile Path
- 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 |
- Also verify Start Command and Health Check Path match the API
- 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
-
constants.tscreated with connection name - All feature modules import from
constants.ts - Health endpoint at
/{name}-api/healthwith@Public()decorator - Route syntax uses new
*pathformat (not:path(*)) - Local test passes:
curl http://localhost:PORT/{name}-api/health
Railway Setup
- Project created in Railway
- Empty service added:
backend-{name}-api - Project token generated and saved to
.env.local - All env vars configured (JWT_SECRET, MONGODB, ALLOWED_ORIGINS)
Deployment Files
-
Dockerfile.railway.{name}created indeployment/ -
railway.{name}.tomlcreated indeployment/ -
deploy-{name}-api.shcreated and executable
⚠️ Railway Dashboard Verification (CRITICAL!)
- Dockerfile Path =
deployment/Dockerfile.railway.{name}(NOT another API's Dockerfile!) - Start Command =
node dist/apis/backend-{name}-api/main.js - Health Check Path =
/{name}-api/health - Builder is set to
Dockerfile(NOT Nixpacks or Railpack)
Post-Deployment
- Check Railway logs for errors
- VERIFY logs show correct API name (e.g.,
[DOCS-API]not[AI-API]) - Test health endpoint:
curl https://YOUR-DOMAIN.railway.app/{name}-api/health - Verify MongoDB connection in logs
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 |