mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-17 05:57:21 -05:00
Add Railway service scaffolding templates
This commit is contained in:
718
templates/RAILWAY_SERVICE_TEMPLATES.md
Normal file
718
templates/RAILWAY_SERVICE_TEMPLATES.md
Normal file
@@ -0,0 +1,718 @@
|
||||
# Railway Service Templates
|
||||
|
||||
This document captures copy-pasteable service scaffolds that can be dropped into individual repositories and deployed directly to Railway. It includes two fully fleshed out anchor services plus slimmer patterns for common variations (Postgres + Prisma, Redis utilities, and a minimal hello world).
|
||||
|
||||
## Anchor 1: FastAPI service (`fastapi-production-3753`)
|
||||
|
||||
**Repository layout**
|
||||
|
||||
```text
|
||||
fastapi-service/
|
||||
├── app/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py
|
||||
│ ├── core/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── config.py
|
||||
│ └── api/
|
||||
│ ├── __init__.py
|
||||
│ ├── deps.py
|
||||
│ └── routes/
|
||||
│ ├── __init__.py
|
||||
│ ├── root.py
|
||||
│ ├── health.py
|
||||
│ └── version.py
|
||||
├── .env.example
|
||||
├── Dockerfile
|
||||
├── pyproject.toml
|
||||
├── railway.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
**pyproject.toml**
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "fastapi-service"
|
||||
version = "1.0.0"
|
||||
description = "FastAPI service for Railway"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"fastapi==0.114.0",
|
||||
"uvicorn[standard]==0.30.0",
|
||||
"python-dotenv==1.0.1",
|
||||
"pydantic-settings==2.3.4"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"ruff",
|
||||
"pytest"
|
||||
]
|
||||
|
||||
[tool.uvicorn]
|
||||
factory = false
|
||||
reload = false
|
||||
```
|
||||
|
||||
**.env.example**
|
||||
|
||||
```bash
|
||||
PORT=8080
|
||||
ENVIRONMENT=production
|
||||
SERVICE_NAME=fastapi-service
|
||||
COMMIT_SHA=
|
||||
```
|
||||
|
||||
**app/core/config.py**
|
||||
|
||||
```python
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
port: int = 8080
|
||||
environment: str = "production"
|
||||
service_name: str = "fastapi-service"
|
||||
commit_sha: str | None = None
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = "utf-8"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
```
|
||||
|
||||
**app/api/routes/root.py**
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter
|
||||
from app.core.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", summary="Root info")
|
||||
async def root():
|
||||
return {
|
||||
"service": settings.service_name,
|
||||
"status": "ok",
|
||||
"environment": settings.environment,
|
||||
}
|
||||
```
|
||||
|
||||
**app/api/routes/health.py**
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/health", summary="Health check")
|
||||
async def health():
|
||||
return {"status": "healthy"}
|
||||
```
|
||||
|
||||
**app/api/routes/version.py**
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter
|
||||
from app.core.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/version", summary="Version info")
|
||||
async def version():
|
||||
return {
|
||||
"version": "1.0.0",
|
||||
"commit": settings.commit_sha,
|
||||
}
|
||||
```
|
||||
|
||||
**app/api/routes/__init__.py**
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter
|
||||
from . import root, health, version
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(root.router)
|
||||
api_router.include_router(health.router)
|
||||
api_router.include_router(version.router)
|
||||
```
|
||||
|
||||
**app/api/deps.py**
|
||||
|
||||
```python
|
||||
# Placeholder for future dependencies (DB sessions, auth, etc.)
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
|
||||
async def get_dummy_dep() -> AsyncGenerator[None, None]:
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
return
|
||||
```
|
||||
|
||||
**app/main.py**
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
from app.api.routes import api_router
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
app = FastAPI(
|
||||
title=settings.service_name,
|
||||
version="1.0.0",
|
||||
)
|
||||
app.include_router(api_router)
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
```
|
||||
|
||||
**Dockerfile**
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
COPY pyproject.toml ./
|
||||
RUN pip install --no-cache-dir uvicorn fastapi python-dotenv pydantic-settings
|
||||
|
||||
COPY app ./app
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
|
||||
```
|
||||
|
||||
**railway.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"build": "pip install --no-cache-dir uvicorn fastapi python-dotenv pydantic-settings",
|
||||
"start": "uvicorn app.main:app --host 0.0.0.0 --port 8080",
|
||||
"healthcheckPath": "/health",
|
||||
"port": 8080
|
||||
}
|
||||
```
|
||||
|
||||
**README.md**
|
||||
|
||||
```markdown
|
||||
# FastAPI Service
|
||||
|
||||
FastAPI microservice for Railway.
|
||||
|
||||
## Running locally
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8080
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
* `GET /` – basic info
|
||||
* `GET /health` – healthcheck
|
||||
* `GET /version` – version info
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anchor 2: Serene Success Node/Express TS (`serene-success-production`)
|
||||
|
||||
**Repository layout**
|
||||
|
||||
```text
|
||||
serene-success-service/
|
||||
├── src/
|
||||
│ ├── index.ts
|
||||
│ ├── config/
|
||||
│ │ └── env.ts
|
||||
│ ├── middleware/
|
||||
│ │ ├── logging.ts
|
||||
│ │ └── errorHandler.ts
|
||||
│ ├── routes/
|
||||
│ │ ├── index.ts
|
||||
│ │ ├── health.ts
|
||||
│ │ └── version.ts
|
||||
│ ├── services/
|
||||
│ │ └── statusService.ts
|
||||
│ └── utils/
|
||||
│ └── logger.ts
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── nodemon.json
|
||||
├── .env.example
|
||||
├── Dockerfile
|
||||
├── railway.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
**package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "serene-success-service",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "nodemon src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"morgan": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.14.2",
|
||||
"nodemon": "^3.1.4",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**tsconfig.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
"module": "commonjs",
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
```
|
||||
|
||||
**.env.example**
|
||||
|
||||
```bash
|
||||
PORT=8080
|
||||
NODE_ENV=production
|
||||
SERVICE_NAME=serene-success
|
||||
COMMIT_SHA=
|
||||
```
|
||||
|
||||
**src/config/env.ts**
|
||||
|
||||
```ts
|
||||
export const env = {
|
||||
port: parseInt(process.env.PORT || "8080", 10),
|
||||
nodeEnv: process.env.NODE_ENV || "development",
|
||||
serviceName: process.env.SERVICE_NAME || "serene-success",
|
||||
commitSha: process.env.COMMIT_SHA || null
|
||||
};
|
||||
```
|
||||
|
||||
**src/utils/logger.ts**
|
||||
|
||||
```ts
|
||||
/* Simple logger wrapper */
|
||||
export const logger = {
|
||||
info: (...args: unknown[]) => console.log("[INFO]", ...args),
|
||||
error: (...args: unknown[]) => console.error("[ERROR]", ...args),
|
||||
warn: (...args: unknown[]) => console.warn("[WARN]", ...args)
|
||||
};
|
||||
```
|
||||
|
||||
**src/middleware/logging.ts**
|
||||
|
||||
```ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export function requestLogger(req: Request, res: Response, next: NextFunction) {
|
||||
const start = Date.now();
|
||||
res.on("finish", () => {
|
||||
const ms = Date.now() - start;
|
||||
logger.info(`${req.method} ${req.originalUrl} -> ${res.statusCode} (${ms}ms)`);
|
||||
});
|
||||
next();
|
||||
}
|
||||
```
|
||||
|
||||
**src/middleware/errorHandler.ts**
|
||||
|
||||
```ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export function errorHandler(
|
||||
err: unknown,
|
||||
_req: Request,
|
||||
res: Response,
|
||||
_next: NextFunction
|
||||
) {
|
||||
logger.error("Unhandled error:", err);
|
||||
res.status(500).json({
|
||||
error: {
|
||||
message: "Internal server error"
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**src/services/statusService.ts**
|
||||
|
||||
```ts
|
||||
import { env } from "../config/env";
|
||||
|
||||
export function getStatus() {
|
||||
return {
|
||||
service: env.serviceName,
|
||||
status: "ok",
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**src/routes/health.ts**
|
||||
|
||||
```ts
|
||||
import { Router } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/health", (_req, res) => {
|
||||
res.json({ status: "healthy" });
|
||||
});
|
||||
|
||||
export default router;
|
||||
```
|
||||
|
||||
**src/routes/version.ts**
|
||||
|
||||
```ts
|
||||
import { Router } from "express";
|
||||
import { env } from "../config/env";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/version", (_req, res) => {
|
||||
res.json({
|
||||
version: "1.0.0",
|
||||
commit: env.commitSha
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
```
|
||||
|
||||
**src/routes/index.ts**
|
||||
|
||||
```ts
|
||||
import { Router } from "express";
|
||||
import { getStatus } from "../services/statusService";
|
||||
import healthRouter from "./health";
|
||||
import versionRouter from "./version";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", (_req, res) => {
|
||||
res.json(getStatus());
|
||||
});
|
||||
|
||||
router.use(healthRouter);
|
||||
router.use(versionRouter);
|
||||
|
||||
export default router;
|
||||
```
|
||||
|
||||
**src/index.ts**
|
||||
|
||||
```ts
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import { env } from "./config/env";
|
||||
import { logger } from "./utils/logger";
|
||||
import { requestLogger } from "./middleware/logging";
|
||||
import { errorHandler } from "./middleware/errorHandler";
|
||||
import routes from "./routes";
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(requestLogger);
|
||||
|
||||
app.use(routes);
|
||||
|
||||
app.use(errorHandler);
|
||||
|
||||
app.listen(env.port, () => {
|
||||
logger.info(`Serene Success listening on port ${env.port}`);
|
||||
});
|
||||
```
|
||||
|
||||
**Dockerfile**
|
||||
|
||||
```dockerfile
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json* tsconfig.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY src ./src
|
||||
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
```
|
||||
|
||||
**railway.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"build": "npm install && npm run build",
|
||||
"start": "npm run start",
|
||||
"healthcheckPath": "/health",
|
||||
"port": 8080
|
||||
}
|
||||
```
|
||||
|
||||
**README.md**
|
||||
|
||||
```markdown
|
||||
# Serene Success Service
|
||||
|
||||
Node.js + Express + TypeScript service for Railway.
|
||||
|
||||
## Local dev
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
* `GET /` – status JSON
|
||||
* `GET /health`
|
||||
* `GET /version`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Slim patterns you can mix in
|
||||
|
||||
These snippets plug into the Node/Express skeleton above:
|
||||
|
||||
### Postgres + Prisma (add to `serene-success` skeleton)
|
||||
|
||||
- `prisma/schema.prisma`
|
||||
- `src/db/prisma.ts`
|
||||
- `src/services/userService.ts`
|
||||
- `src/routes/users.ts` (wire into `routes/index.ts`)
|
||||
|
||||
**prisma/schema.prisma**
|
||||
|
||||
```prisma
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
name String?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
```
|
||||
|
||||
**src/db/prisma.ts**
|
||||
|
||||
```ts
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default prisma;
|
||||
```
|
||||
|
||||
**src/services/userService.ts**
|
||||
|
||||
```ts
|
||||
import prisma from "../db/prisma";
|
||||
|
||||
export async function listUsers() {
|
||||
return prisma.user.findMany();
|
||||
}
|
||||
|
||||
export async function createUser(email: string, name?: string) {
|
||||
return prisma.user.create({
|
||||
data: { email, name }
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**src/routes/users.ts**
|
||||
|
||||
```ts
|
||||
import { Router } from "express";
|
||||
import { listUsers, createUser } from "../services/userService";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/users", async (_req, res, next) => {
|
||||
try {
|
||||
const users = await listUsers();
|
||||
res.json({ users });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/users", async (req, res, next) => {
|
||||
try {
|
||||
const { email, name } = req.body;
|
||||
if (!email) {
|
||||
return res.status(400).json({ error: "email is required" });
|
||||
}
|
||||
const user = await createUser(email, name);
|
||||
res.status(201).json({ user });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
```
|
||||
|
||||
### Redis utility routes
|
||||
|
||||
Add `src/redis/client.ts` and `src/routes/cache.ts`, then mount `cacheRouter` in `routes/index.ts`.
|
||||
|
||||
**src/redis/client.ts**
|
||||
|
||||
```ts
|
||||
import { createClient } from "redis";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
const url = process.env.REDIS_URL;
|
||||
if (!url) {
|
||||
logger.warn("REDIS_URL not set, Redis client will fail to connect.");
|
||||
}
|
||||
|
||||
export const redis = createClient({ url });
|
||||
|
||||
redis.on("error", (err) => logger.error("Redis error:", err));
|
||||
|
||||
export async function connectRedis() {
|
||||
if (!redis.isOpen) {
|
||||
await redis.connect();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**src/routes/cache.ts**
|
||||
|
||||
```ts
|
||||
import { Router } from "express";
|
||||
import { redis, connectRedis } from "../redis/client";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/cache", async (req, res, next) => {
|
||||
try {
|
||||
const { key, value, ttlSeconds } = req.body;
|
||||
if (!key || value === undefined) {
|
||||
return res.status(400).json({ error: "key and value required" });
|
||||
}
|
||||
await connectRedis();
|
||||
if (ttlSeconds) {
|
||||
await redis.set(key, value, { EX: Number(ttlSeconds) });
|
||||
} else {
|
||||
await redis.set(key, value);
|
||||
}
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/cache/:key", async (req, res, next) => {
|
||||
try {
|
||||
await connectRedis();
|
||||
const value = await redis.get(req.params.key);
|
||||
if (value === null) {
|
||||
return res.status(404).json({ error: "not found" });
|
||||
}
|
||||
res.json({ key: req.params.key, value });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
```
|
||||
|
||||
### Hello World minimal (`hello-world-production-789d`)
|
||||
|
||||
A barebones Express variant using the same `package.json`/`Dockerfile` scaffold:
|
||||
|
||||
```ts
|
||||
import express from "express";
|
||||
|
||||
const app = express();
|
||||
const port = parseInt(process.env.PORT || "8080", 10);
|
||||
|
||||
app.get("/", (_req, res) => {
|
||||
res.json({ message: "Hello, World" });
|
||||
});
|
||||
|
||||
app.get("/health", (_req, res) => {
|
||||
res.json({ status: "healthy" });
|
||||
});
|
||||
|
||||
app.get("/version", (_req, res) => {
|
||||
res.json({ version: "1.0.0" });
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Hello World service listening on ${port}`);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to extend for other service names
|
||||
|
||||
Use the Node/Express scaffold as a base and tweak routes/configuration:
|
||||
|
||||
- `nodejs-production-2a66`: duplicate the Serene Success layout, rename the service, and add any bespoke routes you need.
|
||||
- `fantastic-ambition-production-d0de`: start from the same base, add an `/orchestrate` route plus any client wrappers (e.g., `clients/fastapiClient.ts`).
|
||||
- `langtrace-client-production`: reuse the scaffold and add an `sdk/LangtraceClient.ts` plus a `/trace` POST route.
|
||||
- `function-bun-production-8c33`: mimic the REST surface (`/`, `/health`, `/version`) using `Bun.serve` instead of Express.
|
||||
|
||||
Each template keeps `/health` for Railway health checks and assumes port `8080`; adjust in `.env.example` and `railway.json` if needed.
|
||||
Reference in New Issue
Block a user