14 KiB
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
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
[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
PORT=8080
ENVIRONMENT=production
SERVICE_NAME=fastapi-service
COMMIT_SHA=
app/core/config.py
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
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
from fastapi import APIRouter
router = APIRouter()
@router.get("/health", summary="Health check")
async def health():
return {"status": "healthy"}
app/api/routes/version.py
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
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
# 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
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
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
{
"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
# 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 infoGET /health– healthcheckGET /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
{
"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
{
"compilerOptions": {
"target": "ES2019",
"module": "commonjs",
"rootDir": "src",
"outDir": "dist",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
},
"include": ["src"]
}
.env.example
PORT=8080
NODE_ENV=production
SERVICE_NAME=serene-success
COMMIT_SHA=
src/config/env.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
/* 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
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
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
import { env } from "../config/env";
export function getStatus() {
return {
service: env.serviceName,
status: "ok",
timestamp: new Date().toISOString()
};
}
src/routes/health.ts
import { Router } from "express";
const router = Router();
router.get("/health", (_req, res) => {
res.json({ status: "healthy" });
});
export default router;
src/routes/version.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
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
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
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
{
"build": "npm install && npm run build",
"start": "npm run start",
"healthcheckPath": "/health",
"port": 8080
}
README.md
# 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 JSONGET /healthGET /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
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export default prisma;
src/services/userService.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
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
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
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:
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/orchestrateroute plus any client wrappers (e.g.,clients/fastapiClient.ts).langtrace-client-production: reuse the scaffold and add ansdk/LangtraceClient.tsplus a/tracePOST route.function-bun-production-8c33: mimic the REST surface (/,/health,/version) usingBun.serveinstead of Express.
Each template keeps /health for Railway health checks and assumes port 8080; adjust in .env.example and railway.json if needed.