Add Railway service scaffolding templates

This commit is contained in:
Alexa Amundson
2025-11-20 17:17:01 -06:00
parent 48a3ebde9a
commit b3ce0270e8

View 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.