DEV Community

BeanBean
BeanBean

Posted on • Originally published at nextfuture.io.vn

How to deploy a Hono API to Railway with Postgres in 10 minutes (2026)

Originally published on NextFuture

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, then railway login
  • drizzle-kit for 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 install
Enter fullscreen mode Exit fullscreen mode

Step 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/node
Enter fullscreen mode Exit fullscreen mode

Step 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 push
Enter fullscreen mode Exit fullscreen mode

Step 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.app
Enter fullscreen mode Exit fullscreen mode

Full 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 });
Enter fullscreen mode Exit fullscreen mode

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/items
Enter fullscreen mode Exit fullscreen mode

Troubleshooting

  • Build fails: cannot find module postgres — Ensure postgres is under dependencies (not devDependencies) in package.json; Railway installs production deps only by default (override via NIXPACKS_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 variables before deploying.
  • Health check timeout and deployment rollback — Railway assigns a dynamic PORT env var; hardcoding 3000 means the health probe never gets a response. Reading process.env.PORT as 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.
{"@context":"https://schema.org","@type":"HowTo","name":"How to deploy a Hono API to Railway with Postgres in 10 minutes (2026)","step":[{"@type":"HowToStep","position":1,"name":"Bootstrap the Hono project","text":"Run npm create hono@latest, select the nodejs adapter at the prompt, then npm install."},{"@type":"HowToStep","position":2,"name":"Add Drizzle ORM and the Postgres driver","text":"Install drizzle-orm, the postgres driver, dotenv, and drizzle-kit as a devDependency."},{"@type":"HowToStep","position":3,"name":"Define the schema and wire the routes","text":"Create a pgTable schema in src/db/schema.ts and import it into your Hono route handlers in src/index.ts."},{"@type":"HowToStep","position":4,"name":"Provision Railway Postgres","text":"Add a PostgreSQL plugin in the Railway dashboard; DATABASE_URL is injected automatically. Run railway run npx drizzle-kit push to apply the schema."},{"@type":"HowToStep","position":5,"name":"Deploy","text":"Set the Start Command to node dist/index.js and run railway up --detach to ship your API and receive a public HTTPS URL."}]}


This article was originally published on NextFuture. Follow us for more fullstack & AI engineering content.

Top comments (0)