/ astro-integrations / How to Use Neon with Astro: Complete Guide
astro-integrations 5 min read

How to Use Neon with Astro: Complete Guide

Step-by-step guide to integrating Neon with your Astro website.

Neon is serverless Postgres with some features that make it particularly interesting for modern web development. It supports database branching (like Git branches, but for your database), autoscaling that scales to zero when idle, and a generous free tier. For Astro developers who need a real database but do not want to manage infrastructure, Neon is one of the most developer-friendly options available.

The integration pattern is straightforward: connect to Neon's Postgres instance from your Astro server-side code (API routes, SSR pages, or build scripts) using any Postgres client. Neon is just Postgres under the hood, so any library that speaks the Postgres protocol works.

Prerequisites

  • Node.js 18+
  • An Astro project (npm create astro@latest)
  • A Neon account (free tier includes 0.5GB storage, Pro starts at $19/mo)
  • Basic SQL knowledge

Installation

Install the Neon serverless driver and an ORM or query builder. Here are two approaches:

Option 1: Neon's serverless driver (lightweight, optimized for serverless):

npm install @neondatabase/serverless

Option 2: Drizzle ORM with Neon (type-safe queries with migrations):

npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit

Configuration

Get your connection string from the Neon dashboard. Go to your project, click on your database, and copy the connection string.

Add it to .env:

DATABASE_URL=postgresql://username:password@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require

Create a database client. Using Neon's serverless driver:

// src/lib/db.ts
import { neon } from "@neondatabase/serverless";

const sql = neon(import.meta.env.DATABASE_URL);

export default sql;

Or with Drizzle ORM for type-safe queries:

// src/lib/db.ts
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";

const sql = neon(import.meta.env.DATABASE_URL);
export const db = drizzle(sql, { schema });
// src/lib/schema.ts
import { pgTable, serial, text, timestamp, boolean } from "drizzle-orm/pg-core";

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: text("title").notNull(),
  slug: text("slug").notNull().unique(),
  content: text("content"),
  excerpt: text("excerpt"),
  published: boolean("published").default(false),
  createdAt: timestamp("created_at").defaultNow(),
  updatedAt: timestamp("updated_at").defaultNow(),
});

Make sure your Astro config supports server-side code. You need SSR or hybrid mode:

// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";

export default defineConfig({
  output: "hybrid", // or "server"
  adapter: node({ mode: "standalone" }),
});

Basic Usage

Query posts from Neon and render them. Using the raw SQL approach:

---
// src/pages/blog/index.astro
export const prerender = false;

import sql from "../../lib/db";
import BaseLayout from "../../layouts/BaseLayout.astro";

const posts = await sql`
  SELECT id, title, slug, excerpt, created_at
  FROM posts
  WHERE published = true
  ORDER BY created_at DESC
  LIMIT 50
`;
---

<BaseLayout title="Blog">
  <h1>Blog</h1>
  {posts.map((post) => (
    <article>
      <a href={`/blog/${post.slug}`}>
        <h2>{post.title}</h2>
        <p>{post.excerpt}</p>
        <time>{new Date(post.created_at).toLocaleDateString()}</time>
      </a>
    </article>
  ))}
</BaseLayout>

Using Drizzle ORM:

---
// src/pages/blog/index.astro
export const prerender = false;

import { db } from "../../lib/db";
import { posts } from "../../lib/schema";
import { eq, desc } from "drizzle-orm";
import BaseLayout from "../../layouts/BaseLayout.astro";

const allPosts = await db
  .select()
  .from(posts)
  .where(eq(posts.published, true))
  .orderBy(desc(posts.createdAt))
  .limit(50);
---

<BaseLayout title="Blog">
  <h1>Blog</h1>
  {allPosts.map((post) => (
    <article>
      <a href={`/blog/${post.slug}`}>
        <h2>{post.title}</h2>
        <p>{post.excerpt}</p>
      </a>
    </article>
  ))}
</BaseLayout>

API routes for creating or updating data:

// src/pages/api/posts.ts
import type { APIRoute } from "astro";
import { db } from "../../lib/db";
import { posts } from "../../lib/schema";

export const POST: APIRoute = async ({ request }) => {
  const body = await request.json();

  const [newPost] = await db
    .insert(posts)
    .values({
      title: body.title,
      slug: body.slug,
      content: body.content,
      excerpt: body.excerpt,
      published: body.published || false,
    })
    .returning();

  return new Response(JSON.stringify(newPost), {
    status: 201,
    headers: { "Content-Type": "application/json" },
  });
};

Production Tips

  1. Use database branching for development. Create a Neon branch for each feature or PR. This gives you a full copy of your production database to test against without risking production data. Branches are copy-on-write, so they are fast and cheap.

  2. Take advantage of scale-to-zero. Neon suspends idle databases after 5 minutes on the free tier. The first query after suspension has a ~500ms cold start. For production sites, consider the Pro plan which keeps connections warm, or add a keep-alive query.

  3. Use connection pooling. Enable Neon's built-in connection pooler by adding -pooler to your host in the connection string. This is important for serverless environments where many short-lived connections are created.

  4. Run migrations with Drizzle Kit. Use npx drizzle-kit push for development and npx drizzle-kit migrate for production. Store migration files in version control so database changes are tracked alongside code.

  5. Set up read replicas for heavy reads. Neon supports read replicas that autoscale independently. If your Astro site has heavy read traffic, route read queries to a replica endpoint to keep your primary database responsive.

Alternatives to Consider

  • Supabase if you want Postgres with built-in auth, real-time subscriptions, and storage in a single platform.
  • Turso if you prefer SQLite over Postgres and want embedded replicas for ultra-low latency reads.
  • PlanetScale if you need MySQL with branching workflows and non-blocking schema changes.

Wrapping Up

Neon and Astro pair well for sites that need a real database without the complexity of managing Postgres infrastructure. The serverless driver is optimized for the kind of short-lived connections that SSR and API routes create, and database branching gives you a development workflow that feels as natural as Git branching. The free tier is generous enough for side projects and early-stage products, and the autoscaling means you only pay for what you use as you grow.