DEV Community

Alex Spinov
Alex Spinov

Posted on

Neon Has a Free API: Here's How to Use It for Serverless PostgreSQL

Neon is serverless PostgreSQL that scales to zero — you only pay for what you use. The free tier gives you 0.5 GB storage and 190 compute hours per month. Its API lets you manage databases, branches, and endpoints programmatically.

Why Use Neon?

  • Scales to zero — no cost when idle
  • Database branching — instant copies for development
  • Standard PostgreSQL — use any Postgres client or ORM
  • Free tier generous for side projects and MVPs

Getting Started

Get your API key from console.neon.tech > Account Settings > API Keys:

export NEON_API_KEY="your-api-key"

# List projects
curl -s -H "Authorization: Bearer $NEON_API_KEY" \
  "https://console.neon.tech/api/v2/projects" | jq '.projects[] | {id: .id, name: .name, region_id: .region_id}'

# Create a new project
curl -s -H "Authorization: Bearer $NEON_API_KEY" \
  -H "Content-Type: application/json" \
  -X POST "https://console.neon.tech/api/v2/projects" \
  -d '{"project": {"name": "my-app"}}' | jq '{project_id: .project.id, connection_uri: .connection_uris[0].connection_uri}'
Enter fullscreen mode Exit fullscreen mode

Python Client

import requests
import psycopg2

class NeonClient:
    def __init__(self, api_key):
        self.url = "https://console.neon.tech/api/v2"
        self.headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}

    def list_projects(self):
        resp = requests.get(f"{self.url}/projects", headers=self.headers)
        return resp.json()['projects']

    def create_project(self, name, region='aws-us-east-1'):
        resp = requests.post(f"{self.url}/projects", json={'project': {'name': name, 'region_id': region}}, headers=self.headers)
        return resp.json()

    def create_branch(self, project_id, branch_name, parent_id=None):
        payload = {'branch': {'name': branch_name}}
        if parent_id:
            payload['branch']['parent_id'] = parent_id
        resp = requests.post(f"{self.url}/projects/{project_id}/branches", json=payload, headers=self.headers)
        return resp.json()

    def list_branches(self, project_id):
        resp = requests.get(f"{self.url}/projects/{project_id}/branches", headers=self.headers)
        return resp.json()['branches']

    def delete_branch(self, project_id, branch_id):
        resp = requests.delete(f"{self.url}/projects/{project_id}/branches/{branch_id}", headers=self.headers)
        return resp.status_code == 200

    def get_connection_uri(self, project_id, branch_id=None):
        params = {}
        if branch_id:
            params['branch_id'] = branch_id
        resp = requests.get(f"{self.url}/projects/{project_id}/connection_uri", params=params, headers=self.headers)
        return resp.json()['uri']

# Usage
neon = NeonClient('your-api-key')

for project in neon.list_projects():
    print(f"{project['name']:20s} Region: {project['region_id']}")
Enter fullscreen mode Exit fullscreen mode

Database Branching for Development

def create_dev_environment(neon, project_id, developer_name):
    branch_name = f"dev-{developer_name}"

    # Create a branch (instant copy of production data)
    result = neon.create_branch(project_id, branch_name)
    branch_id = result['branch']['id']

    # Get connection string for this branch
    uri = neon.get_connection_uri(project_id, branch_id)
    print(f"Dev environment ready for {developer_name}:")
    print(f"  Branch: {branch_name}")
    print(f"  Connection: {uri[:50]}...")

    return branch_id, uri

def cleanup_dev_environment(neon, project_id, branch_id):
    neon.delete_branch(project_id, branch_id)
    print("Dev environment cleaned up")

# Each developer gets their own database copy
branch_id, uri = create_dev_environment(neon, 'proj_xxx', 'alice')

# When done with feature
cleanup_dev_environment(neon, 'proj_xxx', branch_id)
Enter fullscreen mode Exit fullscreen mode

Safe Schema Migrations

def test_migration(neon, project_id, migration_sql):
    # Create a test branch
    result = neon.create_branch(project_id, 'migration-test')
    branch_id = result['branch']['id']
    uri = neon.get_connection_uri(project_id, branch_id)

    try:
        conn = psycopg2.connect(uri)
        cur = conn.cursor()

        # Run migration on test branch
        cur.execute(migration_sql)
        conn.commit()

        # Verify
        cur.execute("SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'users'")
        print(f"Migration successful! Columns: {cur.fetchone()[0]}")

        cur.close()
        conn.close()
        return True
    except Exception as e:
        print(f"Migration failed: {e}")
        return False
    finally:
        neon.delete_branch(project_id, branch_id)
        print("Test branch cleaned up")

test_migration(neon, 'proj_xxx', 'ALTER TABLE users ADD COLUMN avatar_url TEXT')
Enter fullscreen mode Exit fullscreen mode

Serverless Connection

# Neon's serverless driver works great with edge functions
# No connection pooling needed!

import psycopg2

def query(connection_uri, sql, params=None):
    conn = psycopg2.connect(connection_uri, sslmode='require')
    cur = conn.cursor()
    cur.execute(sql, params)

    if cur.description:
        columns = [desc[0] for desc in cur.description]
        results = [dict(zip(columns, row)) for row in cur.fetchall()]
    else:
        results = None

    conn.commit()
    cur.close()
    conn.close()
    return results

# Usage
users = query(CONNECTION_URI, 'SELECT * FROM users WHERE active = %s LIMIT %s', (True, 10))
for user in users:
    print(f"{user['name']:20s} {user['email']}")
Enter fullscreen mode Exit fullscreen mode

CI/CD Integration

def ci_pipeline(neon, project_id, pr_number):
    branch_name = f"pr-{pr_number}"

    # Create branch for this PR
    result = neon.create_branch(project_id, branch_name)
    branch_id = result['branch']['id']
    uri = neon.get_connection_uri(project_id, branch_id)

    print(f"DATABASE_URL={uri}")
    print(f"Branch '{branch_name}' ready for CI tests")

    # CI runs tests against this branch
    # When PR is merged or closed:
    # neon.delete_branch(project_id, branch_id)

    return uri

ci_pipeline(neon, 'proj_xxx', 42)
Enter fullscreen mode Exit fullscreen mode

Real-World Use Case

A startup used Neon branching in their CI/CD pipeline. Every pull request automatically gets a database branch with production data. Tests run against real data, not fixtures. When the PR is merged, the branch is deleted. They caught 3 migration bugs in the first week that would have caused production outages.

What You Can Build

  • Dev environment manager with instant database copies
  • Migration tester validating schema changes safely
  • CI pipeline with real data for every PR
  • Multi-tenant SaaS with project-per-tenant
  • Analytics sandbox querying production data safely

Need custom database solutions? I build backends, data pipelines, and DevOps tools.

Email me: spinov001@gmail.com
Check out my developer tools: https://apify.com/spinov001

Top comments (0)