Files
blackroad-operating-system/templates/RAILWAY_SERVICE_TEMPLATES.md
2025-11-20 17:17:01 -06:00

719 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.