
How to deploy a Hono API to Railway with Postgres in 10 minutes (2026)
Ship a type-safe Hono v4 API backed by a Railway-provisioned Postgres database in under 10 minutes — no Dockerfile, no config files, free tier included.
The problem
Hono is the fastest Node/Bun-compatible HTTP framework available in 2026 — its benchmark suite regularly outperforms Express and Fastify by a factor of three on raw throughput. The deployment story, however, has historically been messy: you either maintain a custom Dockerfile, wrestle with platform-specific adapters, or fight cold-start limits on serverless edge runtimes that do not support long-lived database connections. Railway solves this by treating your Hono service, its Postgres database, and any other add-ons as first-class siblings in one project, injecting connection strings automatically and handling the build pipeline without extra configuration.
Prerequisites
- Node.js 22+ (confirm with
node -v) or Bun 1.1+ - npm 10+ or Bun package manager
- A Railway account — free tier includes $5/month credit, no credit card required
- Railway CLI:
npm i -g @railway/cli, thenrailway login drizzle-kitfor schema management — installed in Step 2
Step 1: Bootstrap the Hono project
create-hono scaffolds a production-ready tsconfig, a dev watch command via tsx, and a minimal build pipeline in one command — choose the nodejs adapter when prompted, because it is the only option that supports persistent Postgres connections on Railway's always-on containers. Edge and Cloudflare Workers adapters do not allow stateful TCP sockets, so they cannot hold a Postgres connection pool.
npm create hono@latest my-api
# Select "nodejs" template at the prompt
cd my-api
npm installStep 2: Add Drizzle ORM and the Postgres driver
Drizzle's zero-overhead query builder compiles directly to parameterized SQL with no reflection or proxy magic, which means fast cold starts and queries that are straightforward to audit in production logs. The postgres package is the canonical JavaScript driver that Drizzle's drizzle-orm/postgres-js dialect targets — it handles connection pooling, backpressure, and prepared statements natively without a separate pool configuration file.
npm install drizzle-orm postgres dotenv
npm install -D drizzle-kit tsx @types/nodeStep 3: Define the schema and wire the routes
Create src/db/schema.ts using Drizzle's pgTable helper — column definitions carry full TypeScript inference so route handlers get autocomplete on query results without manual type assertions. Your main src/index.ts imports both the Drizzle client and the schema, keeping the database layer a plain import rather than a global singleton that leaks across test files.
Step 4: Provision Railway Postgres
In the Railway dashboard, open your project, choose New Service, then Database, then PostgreSQL. Railway provisions a managed Postgres 16 instance and injects DATABASE_URL as an environment variable into every service in the same project automatically — no secrets panel, no copy-pasting of connection strings, no risk of committing credentials. After linking your repo with railway link, push the Drizzle schema against the remote database before deploying code that depends on it.
railway login
railway link
railway run npx drizzle-kit pushStep 5: Deploy
Set the Start Command in Railway's service settings to node dist/index.js, or add a Procfile at the repo root with web: node dist/index.js — Railway reads either. Running railway up --detach ships the current directory, triggers the Nixpacks build, and prints the public HTTPS URL once the health check passes; total time on a cold push is typically under 90 seconds.
railway up --detach
# Output: Deployment successful → https://my-api-production.up.railway.appFull working example
// src/index.ts — complete Hono + Drizzle API, deploy-ready for Railway
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core";
import { eq } from "drizzle-orm";
import "dotenv/config";
const items = pgTable("items", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
const url = process.env.DATABASE_URL;
if (!url) throw new Error("DATABASE_URL is not set");
const sql = postgres(url, { max: 5 });
const db = drizzle(sql, { schema: { items } });
const app = new Hono();
app.get("/health", (c) => c.json({ status: "ok" }));
app.get("/items", async (c) => {
const rows = await db.select().from(items).orderBy(items.createdAt);
return c.json({ data: rows });
});
app.post("/items", async (c) => {
const body = await c.req.json();
if (!body?.name || typeof body.name !== "string") {
return c.json({ error: "name is required" }, 400);
}
const [row] = await db
.insert(items)
.values({ name: body.name.trim() })
.returning();
return c.json({ data: row }, 201);
});
app.get("/items/:id", async (c) => {
const [row] = await db
.select()
.from(items)
.where(eq(items.id, c.req.param("id")));
if (!row) return c.json({ error: "not found" }, 404);
return c.json({ data: row });
});
const port = Number(process.env.PORT) || 3000;
console.log(`Server listening on port ${port}`);
serve({ fetch: app.fetch, port });
Prefer a managed option? Try Railway — deploy fullstack apps with Postgres, Redis, and auto-SSL in a few clicks, with $5/month free credit and no credit card required.
Testing it
Once railway up completes, copy the URL from the CLI output and run the three commands below. The health endpoint confirms the process started; the POST verifies Postgres connectivity end-to-end; the final GET confirms the row persisted across requests — all three should respond with 2xx status and JSON bodies.
BASE=https://my-api-production.up.railway.app
curl $BASE/health
curl -X POST $BASE/items -H "Content-Type: application/json" -d '{"name":"hello railway"}'
curl $BASE/itemsTroubleshooting
- Build fails: cannot find module postgres — Ensure
postgresis underdependencies(notdevDependencies) inpackage.json; Railway installs production deps only by default (override viaNIXPACKS_NODE_ENVIRONMENT=development). - DATABASE_URL undefined at runtime — The Hono service and the Postgres plugin must be in the same Railway project; verify with
railway variablesbefore deploying. - Health check timeout and deployment rollback — Railway assigns a dynamic
PORTenv var; hardcoding3000means the health probe never gets a response. Readingprocess.env.PORTas shown above is mandatory.
Where to go next
Once the API is live, add a Redis plugin the same way — Railway injects REDIS_URL automatically, and wiring ioredis into Hono middleware takes fewer than 10 lines for request-level caching or per-IP rate limiting. For a broader picture of how platform choices — Railway vs. raw VPS vs. Vercel — compare on the cost-versus-DX axis in 2026, the Q1 2026 Web+AI Recap is the best single read on what actually shifted. The top DX tools for shipping faster in 2026 covers the adjacent tooling — forms, short links, scheduling — that rounds out a modern indie-maker stack built on Railway.
Get weekly highlights
No spam, unsubscribe anytime.
Railway
Deploy fullstack apps effortlessly. Postgres, Redis, Node in just a few clicks.

Comments (0)
Sign in to comment
No comments yet. Be the first to comment!