PostgreSQL API Development Best Practices¶
Objective: Master senior-level PostgreSQL API development patterns for production systems. When you need to build robust APIs with PostgreSQL, when you want to implement RESTful services, when you need enterprise-grade API strategies—these best practices become your weapon of choice.
Core Principles¶
- RESTful Design: Follow REST principles for API design
- Security: Implement proper authentication and authorization
- Performance: Optimize API response times and throughput
- Documentation: Provide comprehensive API documentation
- Versioning: Implement API versioning strategies
API Architecture Patterns¶
RESTful API Design¶
-- Create API endpoints table
CREATE TABLE api_endpoints (
id SERIAL PRIMARY KEY,
endpoint_path VARCHAR(200) UNIQUE NOT NULL,
http_method VARCHAR(10) NOT NULL,
description TEXT,
parameters JSONB,
response_schema JSONB,
authentication_required BOOLEAN DEFAULT TRUE,
rate_limit_per_minute INTEGER DEFAULT 100,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Create function to register API endpoint
CREATE OR REPLACE FUNCTION register_api_endpoint(
p_endpoint_path VARCHAR(200),
p_http_method VARCHAR(10),
p_description TEXT,
p_parameters JSONB DEFAULT '{}',
p_response_schema JSONB DEFAULT '{}',
p_authentication_required BOOLEAN DEFAULT TRUE,
p_rate_limit_per_minute INTEGER DEFAULT 100
)
RETURNS VOID AS $$
BEGIN
INSERT INTO api_endpoints (
endpoint_path, http_method, description, parameters,
response_schema, authentication_required, rate_limit_per_minute
) VALUES (
p_endpoint_path, p_http_method, p_description, p_parameters,
p_response_schema, p_authentication_required, p_rate_limit_per_minute
) ON CONFLICT (endpoint_path)
DO UPDATE SET
http_method = EXCLUDED.http_method,
description = EXCLUDED.description,
parameters = EXCLUDED.parameters,
response_schema = EXCLUDED.response_schema,
authentication_required = EXCLUDED.authentication_required,
rate_limit_per_minute = EXCLUDED.rate_limit_per_minute,
updated_at = CURRENT_TIMESTAMP;
END;
$$ LANGUAGE plpgsql;
-- Register common API endpoints
SELECT register_api_endpoint('/api/v1/users', 'GET', 'Get all users', '{}', '{"users": []}', TRUE, 100);
SELECT register_api_endpoint('/api/v1/users/{id}', 'GET', 'Get user by ID', '{"id": "integer"}', '{"user": {}}', TRUE, 100);
SELECT register_api_endpoint('/api/v1/users', 'POST', 'Create new user', '{"user": {}}', '{"user": {}}', TRUE, 50);
SELECT register_api_endpoint('/api/v1/users/{id}', 'PUT', 'Update user', '{"id": "integer", "user": {}}', '{"user": {}}', TRUE, 50);
SELECT register_api_endpoint('/api/v1/users/{id}', 'DELETE', 'Delete user', '{"id": "integer"}', '{"message": "User deleted"}', TRUE, 25);
API Authentication¶
-- Create API keys table
CREATE TABLE api_keys (
id SERIAL PRIMARY KEY,
key_hash VARCHAR(64) UNIQUE NOT NULL,
key_name VARCHAR(100) NOT NULL,
user_id INTEGER,
permissions JSONB DEFAULT '{}',
rate_limit_per_minute INTEGER DEFAULT 100,
is_active BOOLEAN DEFAULT TRUE,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMPTZ
);
-- Create function to generate API key
CREATE OR REPLACE FUNCTION generate_api_key(
p_key_name VARCHAR(100),
p_user_id INTEGER DEFAULT NULL,
p_permissions JSONB DEFAULT '{}',
p_rate_limit_per_minute INTEGER DEFAULT 100,
p_expires_at TIMESTAMPTZ DEFAULT NULL
)
RETURNS TEXT AS $$
DECLARE
api_key TEXT;
key_hash VARCHAR(64);
BEGIN
-- Generate random API key
api_key := encode(gen_random_bytes(32), 'hex');
key_hash := encode(digest(api_key, 'sha256'), 'hex');
-- Store key hash
INSERT INTO api_keys (
key_hash, key_name, user_id, permissions,
rate_limit_per_minute, expires_at
) VALUES (
key_hash, p_key_name, p_user_id, p_permissions,
p_rate_limit_per_minute, p_expires_at
);
RETURN api_key;
END;
$$ LANGUAGE plpgsql;
-- Create function to validate API key
CREATE OR REPLACE FUNCTION validate_api_key(p_api_key TEXT)
RETURNS TABLE (
is_valid BOOLEAN,
user_id INTEGER,
permissions JSONB,
rate_limit_per_minute INTEGER
) AS $$
DECLARE
key_hash VARCHAR(64);
key_record RECORD;
BEGIN
-- Hash the provided key
key_hash := encode(digest(p_api_key, 'sha256'), 'hex');
-- Look up key
SELECT ak.user_id, ak.permissions, ak.rate_limit_per_minute
INTO key_record
FROM api_keys ak
WHERE ak.key_hash = key_hash
AND ak.is_active = TRUE
AND (ak.expires_at IS NULL OR ak.expires_at > CURRENT_TIMESTAMP);
IF key_record IS NULL THEN
RETURN QUERY SELECT FALSE, NULL::INTEGER, '{}'::JSONB, 0::INTEGER;
ELSE
-- Update last used timestamp
UPDATE api_keys
SET last_used_at = CURRENT_TIMESTAMP
WHERE key_hash = key_hash;
RETURN QUERY SELECT TRUE, key_record.user_id, key_record.permissions, key_record.rate_limit_per_minute;
END IF;
END;
$$ LANGUAGE plpgsql;
API Implementation¶
FastAPI Integration¶
# api/postgres_api.py
from fastapi import FastAPI, HTTPException, Depends, Header
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import psycopg2
import json
from datetime import datetime
from typing import Optional, List
import logging
class PostgreSQLAPI:
def __init__(self, connection_params):
self.conn_params = connection_params
self.setup_logging()
def setup_logging(self):
"""Setup logging."""
logging.basicConfig(level=logging.INFO)
self.logger = logging.getLogger(__name__)
def get_connection(self):
"""Get database connection."""
return psycopg2.connect(**self.conn_params)
def validate_api_key(self, api_key: str):
"""Validate API key."""
conn = self.get_connection()
try:
with conn.cursor() as cur:
cur.execute("SELECT * FROM validate_api_key(%s)", (api_key,))
result = cur.fetchone()
if result and result[0]: # is_valid
return {
'user_id': result[1],
'permissions': result[2],
'rate_limit': result[3]
}
else:
return None
except Exception as e:
self.logger.error(f"Error validating API key: {e}")
return None
finally:
conn.close()
def get_users(self, limit: int = 100, offset: int = 0):
"""Get users with pagination."""
conn = self.get_connection()
try:
with conn.cursor() as cur:
cur.execute("""
SELECT id, username, email, created_at
FROM users
ORDER BY created_at DESC
LIMIT %s OFFSET %s
""", (limit, offset))
users = cur.fetchall()
return [{'id': u[0], 'username': u[1], 'email': u[2], 'created_at': u[3]} for u in users]
except Exception as e:
self.logger.error(f"Error getting users: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
conn.close()
def get_user(self, user_id: int):
"""Get user by ID."""
conn = self.get_connection()
try:
with conn.cursor() as cur:
cur.execute("""
SELECT id, username, email, created_at
FROM users
WHERE id = %s
""", (user_id,))
user = cur.fetchone()
if user:
return {'id': user[0], 'username': user[1], 'email': user[2], 'created_at': user[3]}
else:
raise HTTPException(status_code=404, detail="User not found")
except HTTPException:
raise
except Exception as e:
self.logger.error(f"Error getting user: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
conn.close()
def create_user(self, user_data: dict):
"""Create new user."""
conn = self.get_connection()
try:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO users (username, email, password_hash)
VALUES (%s, %s, %s)
RETURNING id, username, email, created_at
""", (user_data['username'], user_data['email'], user_data['password_hash']))
user = cur.fetchone()
conn.commit()
return {'id': user[0], 'username': user[1], 'email': user[2], 'created_at': user[3]}
except Exception as e:
conn.rollback()
self.logger.error(f"Error creating user: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
conn.close()
def update_user(self, user_id: int, user_data: dict):
"""Update user."""
conn = self.get_connection()
try:
with conn.cursor() as cur:
cur.execute("""
UPDATE users
SET username = %s, email = %s, updated_at = CURRENT_TIMESTAMP
WHERE id = %s
RETURNING id, username, email, created_at
""", (user_data['username'], user_data['email'], user_id))
user = cur.fetchone()
if user:
conn.commit()
return {'id': user[0], 'username': user[1], 'email': user[2], 'created_at': user[3]}
else:
raise HTTPException(status_code=404, detail="User not found")
except HTTPException:
raise
except Exception as e:
conn.rollback()
self.logger.error(f"Error updating user: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
conn.close()
def delete_user(self, user_id: int):
"""Delete user."""
conn = self.get_connection()
try:
with conn.cursor() as cur:
cur.execute("DELETE FROM users WHERE id = %s", (user_id,))
if cur.rowcount > 0:
conn.commit()
return {'message': 'User deleted successfully'}
else:
raise HTTPException(status_code=404, detail="User not found")
except HTTPException:
raise
except Exception as e:
conn.rollback()
self.logger.error(f"Error deleting user: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
conn.close()
# FastAPI application
app = FastAPI(title="PostgreSQL API", version="1.0.0")
security = HTTPBearer()
api = PostgreSQLAPI({
'host': 'localhost',
'database': 'production',
'user': 'api_user',
'password': 'api_password'
})
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Get current user from API key."""
api_key = credentials.credentials
user_info = api.validate_api_key(api_key)
if not user_info:
raise HTTPException(status_code=401, detail="Invalid API key")
return user_info
@app.get("/api/v1/users")
async def get_users(
limit: int = 100,
offset: int = 0,
current_user: dict = Depends(get_current_user)
):
"""Get all users."""
return api.get_users(limit, offset)
@app.get("/api/v1/users/{user_id}")
async def get_user(
user_id: int,
current_user: dict = Depends(get_current_user)
):
"""Get user by ID."""
return api.get_user(user_id)
@app.post("/api/v1/users")
async def create_user(
user_data: dict,
current_user: dict = Depends(get_current_user)
):
"""Create new user."""
return api.create_user(user_data)
@app.put("/api/v1/users/{user_id}")
async def update_user(
user_id: int,
user_data: dict,
current_user: dict = Depends(get_current_user)
):
"""Update user."""
return api.update_user(user_id, user_data)
@app.delete("/api/v1/users/{user_id}")
async def delete_user(
user_id: int,
current_user: dict = Depends(get_current_user)
):
"""Delete user."""
return api.delete_user(user_id)
API Security¶
Rate Limiting¶
-- Create rate limiting table
CREATE TABLE api_rate_limits (
id SERIAL PRIMARY KEY,
api_key_hash VARCHAR(64) NOT NULL,
endpoint_path VARCHAR(200) NOT NULL,
request_count INTEGER DEFAULT 1,
window_start TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Create function to check rate limit
CREATE OR REPLACE FUNCTION check_rate_limit(
p_api_key_hash VARCHAR(64),
p_endpoint_path VARCHAR(200),
p_rate_limit_per_minute INTEGER DEFAULT 100
)
RETURNS BOOLEAN AS $$
DECLARE
current_count INTEGER;
window_start TIMESTAMPTZ;
BEGIN
-- Get current window start
window_start := date_trunc('minute', CURRENT_TIMESTAMP);
-- Get current request count
SELECT COALESCE(SUM(request_count), 0) INTO current_count
FROM api_rate_limits
WHERE api_key_hash = p_api_key_hash
AND endpoint_path = p_endpoint_path
AND window_start >= window_start;
-- Check if limit exceeded
IF current_count >= p_rate_limit_per_minute THEN
RETURN FALSE;
END IF;
-- Record request
INSERT INTO api_rate_limits (api_key_hash, endpoint_path, window_start)
VALUES (p_api_key_hash, p_endpoint_path, window_start)
ON CONFLICT (api_key_hash, endpoint_path, window_start)
DO UPDATE SET request_count = api_rate_limits.request_count + 1;
RETURN TRUE;
END;
$$ LANGUAGE plpgsql;
Input Validation¶
-- Create function to validate input
CREATE OR REPLACE FUNCTION validate_api_input(
p_input_data JSONB,
p_validation_schema JSONB
)
RETURNS BOOLEAN AS $$
DECLARE
required_fields TEXT[];
field_name TEXT;
field_value JSONB;
BEGIN
-- Check required fields
required_fields := ARRAY(SELECT jsonb_array_elements_text(p_validation_schema->'required'));
FOR field_name IN SELECT unnest(required_fields) LOOP
field_value := p_input_data -> field_name;
IF field_value IS NULL OR field_value = 'null' THEN
RAISE EXCEPTION 'Required field % is missing', field_name;
END IF;
END LOOP;
-- Validate field types
IF p_validation_schema ? 'properties' THEN
FOR field_name, field_schema IN
SELECT key, value FROM jsonb_each(p_validation_schema->'properties')
LOOP
field_value := p_input_data -> field_name;
IF field_value IS NOT NULL AND field_value != 'null' THEN
-- Validate string fields
IF field_schema->>'type' = 'string' AND jsonb_typeof(field_value) != 'string' THEN
RAISE EXCEPTION 'Field % must be a string', field_name;
END IF;
-- Validate integer fields
IF field_schema->>'type' = 'integer' AND jsonb_typeof(field_value) != 'number' THEN
RAISE EXCEPTION 'Field % must be an integer', field_name;
END IF;
-- Validate email format
IF field_schema->>'format' = 'email' THEN
IF NOT (field_value #>> '{}' ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$') THEN
RAISE EXCEPTION 'Field % must be a valid email', field_name;
END IF;
END IF;
END IF;
END LOOP;
END IF;
RETURN TRUE;
END;
$$ LANGUAGE plpgsql;
API Documentation¶
OpenAPI Integration¶
# api/openapi_generator.py
import psycopg2
import json
from typing import Dict, List
class OpenAPIGenerator:
def __init__(self, connection_params):
self.conn_params = connection_params
def generate_openapi_spec(self):
"""Generate OpenAPI specification from database."""
conn = psycopg2.connect(**self.conn_params)
try:
with conn.cursor() as cur:
# Get API endpoints
cur.execute("""
SELECT endpoint_path, http_method, description,
parameters, response_schema, authentication_required
FROM api_endpoints
ORDER BY endpoint_path
""")
endpoints = cur.fetchall()
# Generate OpenAPI spec
openapi_spec = {
"openapi": "3.0.0",
"info": {
"title": "PostgreSQL API",
"version": "1.0.0",
"description": "API for PostgreSQL database operations"
},
"servers": [
{"url": "https://api.example.com", "description": "Production server"}
],
"security": [
{"ApiKeyAuth": []}
],
"paths": self.generate_paths(endpoints),
"components": {
"securitySchemes": {
"ApiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "Authorization"
}
}
}
}
return openapi_spec
except Exception as e:
print(f"Error generating OpenAPI spec: {e}")
return {}
finally:
conn.close()
def generate_paths(self, endpoints):
"""Generate OpenAPI paths from endpoints."""
paths = {}
for endpoint_path, http_method, description, parameters, response_schema, auth_required in endpoints:
if endpoint_path not in paths:
paths[endpoint_path] = {}
paths[endpoint_path][http_method.lower()] = {
"summary": description,
"security": [{"ApiKeyAuth": []}] if auth_required else [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": response_schema
}
}
},
"401": {"description": "Unauthorized"},
"404": {"description": "Not found"},
"500": {"description": "Internal server error"}
}
}
return paths
API Monitoring¶
API Metrics¶
-- Create API metrics table
CREATE TABLE api_metrics (
id SERIAL PRIMARY KEY,
endpoint_path VARCHAR(200) NOT NULL,
http_method VARCHAR(10) NOT NULL,
response_time_ms INTEGER NOT NULL,
status_code INTEGER NOT NULL,
api_key_hash VARCHAR(64),
user_id INTEGER,
request_size_bytes INTEGER,
response_size_bytes INTEGER,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Create function to record API metrics
CREATE OR REPLACE FUNCTION record_api_metrics(
p_endpoint_path VARCHAR(200),
p_http_method VARCHAR(10),
p_response_time_ms INTEGER,
p_status_code INTEGER,
p_api_key_hash VARCHAR(64) DEFAULT NULL,
p_user_id INTEGER DEFAULT NULL,
p_request_size_bytes INTEGER DEFAULT NULL,
p_response_size_bytes INTEGER DEFAULT NULL
)
RETURNS VOID AS $$
BEGIN
INSERT INTO api_metrics (
endpoint_path, http_method, response_time_ms, status_code,
api_key_hash, user_id, request_size_bytes, response_size_bytes
) VALUES (
p_endpoint_path, p_http_method, p_response_time_ms, p_status_code,
p_api_key_hash, p_user_id, p_request_size_bytes, p_response_size_bytes
);
END;
$$ LANGUAGE plpgsql;
-- Create function to get API metrics
CREATE OR REPLACE FUNCTION get_api_metrics(
p_start_date TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP - INTERVAL '1 hour',
p_end_date TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
)
RETURNS TABLE (
endpoint_path VARCHAR(200),
http_method VARCHAR(10),
request_count BIGINT,
avg_response_time NUMERIC,
max_response_time INTEGER,
min_response_time INTEGER,
success_rate NUMERIC,
error_count BIGINT
) AS $$
BEGIN
RETURN QUERY
SELECT
am.endpoint_path,
am.http_method,
COUNT(*) as request_count,
AVG(am.response_time_ms) as avg_response_time,
MAX(am.response_time_ms) as max_response_time,
MIN(am.response_time_ms) as min_response_time,
(COUNT(*) FILTER (WHERE am.status_code < 400)::NUMERIC / COUNT(*)::NUMERIC * 100) as success_rate,
COUNT(*) FILTER (WHERE am.status_code >= 400) as error_count
FROM api_metrics am
WHERE am.created_at BETWEEN p_start_date AND p_end_date
GROUP BY am.endpoint_path, am.http_method
ORDER BY request_count DESC;
END;
$$ LANGUAGE plpgsql;
TL;DR Runbook¶
Quick Start¶
-- 1. Register API endpoints
SELECT register_api_endpoint('/api/v1/users', 'GET', 'Get all users');
-- 2. Generate API key
SELECT generate_api_key('my_api_key', 1, '{"read": true, "write": true}');
-- 3. Validate API key
SELECT * FROM validate_api_key('your_api_key_here');
-- 4. Check rate limits
SELECT check_rate_limit('key_hash', '/api/v1/users', 100);
-- 5. Record API metrics
SELECT record_api_metrics('/api/v1/users', 'GET', 150, 200);
Essential Patterns¶
# Complete PostgreSQL API development setup
def setup_postgresql_api_development():
# 1. RESTful API design
# 2. API authentication
# 3. API implementation
# 4. API security
# 5. Input validation
# 6. API documentation
# 7. API monitoring
# 8. Rate limiting
print("PostgreSQL API development setup complete!")
This guide provides the complete machinery for PostgreSQL API development excellence. Each pattern includes implementation examples, API strategies, and real-world usage patterns for enterprise PostgreSQL API systems.