JSTAcademy
0 XP
Dashboard
Crash Courses
Deploying a Separate Backend API
14 min
Masters+150 XP
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

  1. Push your code to GitHub
  2. railway.app → New Project → Deploy from GitHub repo
  3. Railway auto-detects Node.js or uses your Dockerfile
  4. 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

  1. Railway provisions a *.up.railway.app domain 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:

  1. Is your Vercel URL in the origin allowlist?
  2. Is credentials: true set in both the CORS config AND the fetch call?
  3. Is the OPTIONS preflight request handled? (express cors() handles this automatically)
  4. 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,
}))
0%