8.5 KiB
8.5 KiB
Neon JS Data API Reference
Complete reference for PostgREST-style database queries using @neondatabase/neon-js.
Client Setup
Next.js
// lib/db/client.ts
import { createClient } from "@neondatabase/neon-js";
import type { Database } from "./database.types";
export const dbClient = createClient<Database>({
auth: { url: process.env.NEXT_PUBLIC_NEON_AUTH_URL! },
dataApi: { url: process.env.NEON_DATA_API_URL! },
});
React SPA
import { createClient } from "@neondatabase/neon-js";
import { BetterAuthReactAdapter } from "@neondatabase/neon-js/auth/react/adapters";
const client = createClient<Database>({
auth: {
adapter: BetterAuthReactAdapter(),
url: import.meta.env.VITE_NEON_AUTH_URL,
},
dataApi: { url: import.meta.env.VITE_NEON_DATA_API_URL },
});
Node.js Backend
import { createClient } from "@neondatabase/neon-js";
const client = createClient<Database>({
auth: { url: process.env.NEON_AUTH_URL! },
dataApi: { url: process.env.NEON_DATA_API_URL! },
});
Query Patterns
All query methods follow PostgREST syntax (same as Supabase).
Select Queries
Basic select:
const { data, error } = await client.from("items").select();
Select specific columns:
const { data } = await client.from("items").select("id, name, status");
Select with filters:
const { data } = await client
.from("items")
.select("id, name, status")
.eq("status", "active")
.order("created_at", { ascending: false })
.limit(10);
Select single row:
const { data, error } = await client
.from("items")
.select("*")
.eq("id", 1)
.single();
Insert
Insert single row:
const { data, error } = await client
.from("items")
.insert({ name: "New Item", status: "pending" })
.select()
.single();
Insert multiple rows:
const { data, error } = await client
.from("items")
.insert([
{ name: "Item 1", status: "pending" },
{ name: "Item 2", status: "pending" },
])
.select();
Update
Update with filter:
await client.from("items").update({ status: "completed" }).eq("id", 1);
Update and return data:
const { data, error } = await client
.from("items")
.update({ status: "completed" })
.eq("id", 1)
.select()
.single();
Delete
Delete single row:
await client.from("items").delete().eq("id", 1);
Delete and return data:
const { data, error } = await client
.from("items")
.delete()
.eq("id", 1)
.select()
.single();
Upsert
await client
.from("items")
.upsert({ id: 1, name: "Updated Item", status: "active" });
Filtering
Comparison Operators
// Equal
.eq("status", "active")
// Not equal
.neq("status", "archived")
// Greater than
.gt("price", 100)
// Greater than or equal
.gte("price", 100)
// Less than
.lt("price", 100)
// Less than or equal
.lte("price", 100)
// Like (pattern matching)
.like("name", "%item%")
// ILike (case-insensitive)
.ilike("name", "%item%")
// Is null
.is("deleted_at", null)
// Is not null
.not("deleted_at", "is", null)
// In array
.in("status", ["active", "pending"])
// Contains (for arrays/JSONB)
.contains("tags", ["important"])
Logical Operators
// AND (chained)
.eq("status", "active")
.gt("price", 100)
// OR
.or("status.eq.active,price.gt.100")
// NOT
.not("status", "eq", "archived")
Ordering
// Ascending
.order("created_at", { ascending: true })
// Descending
.order("created_at", { ascending: false })
// Multiple columns
.order("status", { ascending: true })
.order("created_at", { ascending: false })
Pagination
// Limit
.limit(10)
// Range (offset + limit)
.range(0, 9) // First 10 items
// Range for pagination
const page = 1;
const pageSize = 10;
.range((page - 1) * pageSize, page * pageSize - 1)
Relationships
Select with Relationships
One-to-many:
const { data } = await client
.from("posts")
.select("id, title, author:users(name, email)");
Many-to-many:
const { data } = await client
.from("posts")
.select("id, title, tags:post_tags(tag:tags(name))");
Nested relationships:
const { data } = await client.from("posts").select(`
id,
title,
author:users(
id,
name,
profile:profiles(bio, avatar)
)
`);
Type Generation
Generate TypeScript types from your database schema:
npx neon-js gen-types --db-url "postgresql://user:pass@host/db" --output src/types/database.ts
Or using environment variable:
npx neon-js gen-types --db-url "$DATABASE_URL" --output lib/db/database.types.ts
Use types in client:
import { createClient } from "@neondatabase/neon-js";
import type { Database } from "./database.types";
export const dbClient = createClient<Database>({
auth: { url: process.env.NEXT_PUBLIC_NEON_AUTH_URL! },
dataApi: { url: process.env.NEON_DATA_API_URL! },
});
Benefits:
- Full TypeScript autocomplete for tables and columns
- Type-safe queries
- Compile-time error checking
Error Handling
Check for errors:
const { data, error } = await client.from("items").select();
if (error) {
console.error("Database error:", error.message);
console.error("Error code:", error.code);
console.error("Error details:", error.details);
return;
}
// Use data
console.log(data);
Common error codes:
PGRST116- No rows returned (when using.single())23505- Unique violation23503- Foreign key violation42P01- Table does not exist
Usage Examples
Server Component (Next.js)
// app/posts/page.tsx
import { dbClient } from "@/lib/db/client";
export default async function PostsPage() {
const { data: posts, error } = await dbClient
.from("posts")
.select("id, title, created_at, author:users(name)")
.order("created_at", { ascending: false })
.limit(10);
if (error) return <div>Error loading posts</div>;
return (
<ul>
{posts?.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>By {post.author?.name}</p>
</li>
))}
</ul>
);
}
API Route (Next.js)
// app/api/posts/route.ts
import { dbClient } from "@/lib/db/client";
import { NextResponse } from "next/server";
export async function GET() {
const { data, error } = await dbClient.from("posts").select();
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(data);
}
export async function POST(request: Request) {
const body = await request.json();
const { data, error } = await dbClient
.from("posts")
.insert(body)
.select()
.single();
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
return NextResponse.json(data, { status: 201 });
}
Client Component (React)
"use client";
import { useEffect, useState } from "react";
import { dbClient } from "@/lib/db/client";
export function ItemsList() {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchItems() {
const { data, error } = await dbClient
.from("items")
.select("id, name, status")
.eq("status", "active");
if (error) {
console.error(error);
return;
}
setItems(data || []);
setLoading(false);
}
fetchItems();
}, []);
if (loading) return <div>Loading...</div>;
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
Supabase Migration
The Neon JS SDK uses the same PostgREST API as Supabase, making migration straightforward:
Before (Supabase):
import { createClient } from "@supabase/supabase-js";
const client = createClient(SUPABASE_URL, SUPABASE_KEY);
After (Neon):
import { createClient, SupabaseAuthAdapter } from "@neondatabase/neon-js";
const client = createClient({
auth: { adapter: SupabaseAuthAdapter(), url: NEON_AUTH_URL },
dataApi: { url: NEON_DATA_API_URL },
});
Query syntax remains the same:
// Works identically in both
await client.auth.signInWithPassword({ email, password });
const { data } = await client.from("items").select();