Crash Courses · Masters
Deploying a Separate Backend API
Ship a standalone Node/Express API to Railway or Render — separate from your Next.js frontend.
14 min read+150 XP on completionCert: Ship It Crash Course
Tap any word in the text below to start reading from there.
Deploying a Separate Backend API
When You Need a Separate API
Next.js API routes handle most backend needs. You need a separate backend when:
- The backend is shared by multiple frontends (web + mobile)
- You need persistent WebSocket connections
- Long-running jobs that exceed Vercel's 60-second function timeout
- You're building a microservice that other teams consume
---
The Minimal Production-Ready Express App
// src/index.ts
import express from 'express'
import cors from 'cors'
const app = express()
// CORS — must come before routes
app.use(cors({
origin: process.env.ALLOWED_ORIGIN || 'http://localhost:3000',
credentials: true,
}))
app.use(express.json())
// Health check — Railway uses this to verify your service is up
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() })
})
// Your routes
app.get('/api/users', async (req, res) => {
// ...
})
// Railway injects PORT — never hardcode
const PORT = process.env.PORT || 3001
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})---
Multi-Stage Dockerfile
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci # exact versions from package-lock.json
COPY . .
RUN npm run build # tsc → compiles TypeScript to dist/
# Stage 2: Production (no devDependencies, no source files)
FROM node:20-alpine AS runner
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev # production dependencies only
COPY --from=builder /app/dist ./dist
EXPOSE 8080
CMD ["node", "dist/index.js"]Why multi-stage? A single-stage image with TypeScript compiler, ts-node, and devDependencies can reach 800MB. The multi-stage final image is ~80MB.
---
Deploying to Railway
- Push your code to GitHub
- railway.app → New Project → Deploy from GitHub repo
- Railway auto-detects Node.js or uses your Dockerfile
- Set environment variables in the Railway dashboard:
- DATABASE_URL (from your Supabase project settings)
- ALLOWED_ORIGIN (your Vercel frontend URL)
- Any API keys your backend needs
- Railway provisions a
*.up.railway.appdomain automatically
Important Railway settings:
Health Check Path: /health
Health Check Timeout: 30s
Restart Policy: Always---
Calling Your Railway API from Next.js
// In a Server Component or API route
const API_URL = process.env.API_URL // https://your-api.up.railway.app
const response = await fetch(`${API_URL}/api/users`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
// Cache behavior
cache: 'no-store', // Always fresh (SSR)
// or
next: { revalidate: 60 } // Re-fetch every 60 seconds (ISR)
})---
The CORS Debug Checklist
If you see Access-Control-Allow-Origin errors:
- Is your Vercel URL in the
originallowlist? - Is
credentials: trueset in both the CORS config AND the fetch call? - Is the OPTIONS preflight request handled? (express cors() handles this automatically)
- Is the API URL correct? (http vs https)
// Explicit CORS for debugging
app.use(cors({
origin: ['https://your-app.vercel.app', 'http://localhost:3000'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
}))Reading progress
0% read
In this module
Dockerfile
Railway
Health Check
CORS
0%