POST /data
Query endpoint request and response format.
The /data endpoint is the single entry point for all data operations. It receives parameterized SQL + params from the Drizzle Proxy client and returns query results. Every request must include a valid JWT.
Request Format
The client sends parameterized SQL built by Drizzle ORM on the client side:
POST /data HTTP/1.1
Content-Type: application/json
Authorization: Bearer <jwt_token>
{
"sql": "SELECT \"id\", \"amount\", \"status\" FROM \"main\".\"orders\" WHERE \"status\" = $1 ORDER BY \"created_at\" DESC LIMIT $2",
"params": ["active", 50],
"method": "all"
}Request Body Fields
| Field | Type | Required | Description |
|---|---|---|---|
sql | string | Yes | Parameterized SQL query built by Drizzle ORM. Uses $1, $2, etc. as placeholders. |
params | any[] | Yes | Parameter values corresponding to the SQL placeholders. |
method | string | Yes | Query execution method. One of: all (returns rows as arrays), get (single row), values (raw values), or run (execute without returning rows). |
Request Examples
SELECT (method: "all")
{
"sql": "SELECT \"id\", \"amount\", \"status\", \"created_at\" FROM \"main\".\"orders\" WHERE \"status\" = $1 AND \"amount\" >= $2 ORDER BY \"created_at\" DESC LIMIT $3 OFFSET $4",
"params": ["active", 100, 20, 0],
"method": "all"
}SELECT single row (method: "get")
{
"sql": "SELECT \"id\", \"amount\", \"status\" FROM \"main\".\"orders\" WHERE \"id\" = $1 LIMIT $2",
"params": ["ord_abc123", 1],
"method": "get"
}INSERT (method: "run")
{
"sql": "INSERT INTO \"main\".\"orders\" (\"amount\", \"status\", \"notes\") VALUES ($1, $2, $3) RETURNING \"id\", \"amount\", \"status\", \"created_at\"",
"params": [250, "draft", "New order"],
"method": "run"
}UPDATE (method: "run")
{
"sql": "UPDATE \"main\".\"orders\" SET \"status\" = $1, \"notes\" = $2 WHERE \"id\" = $3 RETURNING \"id\", \"status\", \"updated_at\"",
"params": ["active", "Approved", "ord_abc123"],
"method": "run"
}DELETE (method: "run")
{
"sql": "DELETE FROM \"main\".\"orders\" WHERE \"id\" = $1",
"params": ["ord_abc123"],
"method": "run"
}COUNT (method: "get")
{
"sql": "SELECT count(*) AS \"count\" FROM \"main\".\"orders\" WHERE \"status\" = $1",
"params": ["active"],
"method": "get"
}Aggregation (method: "all")
{
"sql": "SELECT \"status\", sum(\"amount\") AS \"total\", count(*) AS \"count\", avg(\"amount\") AS \"average\" FROM \"main\".\"orders\" WHERE \"status\" = $1 GROUP BY \"status\"",
"params": ["active"],
"method": "all"
}Response Format
The response follows the Drizzle Proxy protocol. The format depends on the method field in the request.
method: "all"
Returns rows as arrays of values:
{
"rows": [
["ord_001", 250, "active", "2025-03-15T10:00:00Z"],
["ord_002", 100, "active", "2025-03-14T09:00:00Z"]
]
}method: "get"
Returns a single row as an array:
{
"rows": ["ord_001", 250, "active"]
}method: "run"
Returns an empty result (used for writes):
{
"rows": []
}Drizzle ORM on the client maps these raw arrays back into typed objects automatically.
Authentication
Every request to /data must include a valid JWT in the Authorization header:
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...If the token is missing, invalid, or expired, the server responds with 401 Unauthorized:
{
"error": "UNAUTHORIZED",
"message": "Invalid or expired token.",
"correlation_id": "req_xyz789"
}Error Format
All errors follow the same shape:
interface ErrorResponse {
/** Error code for programmatic handling */
error: string
/** Human-readable error message */
message: string
/** Unique request ID for log correlation */
correlation_id: string
/** Additional details (development mode only) */
details?: Record<string, any>
}Error Codes
| HTTP Status | Error Code | Description |
|---|---|---|
400 | BAD_REQUEST | Invalid request body, missing required fields, or malformed SQL. |
401 | UNAUTHORIZED | Missing, invalid, or expired JWT token. |
403 | PERMISSION_DENIED | The user does not have permission for this table/operation. |
404 | TABLE_NOT_FOUND | The specified table does not exist in any connection. |
408 | QUERY_TIMEOUT | The query exceeded the configured timeout. |
413 | PAYLOAD_TOO_LARGE | The request body exceeds limits.maxRequestBodySize. |
422 | VALIDATION_ERROR | Write data failed the permission's check validation. |
429 | RATE_LIMITED | The user or IP has exceeded the rate limit. |
500 | INTERNAL_ERROR | An unexpected server error occurred. |
Error Example
{
"error": "PERMISSION_DENIED",
"message": "You do not have permission to perform this action.",
"correlation_id": "req_abc123"
}In development mode (NODE_ENV !== 'production'), the response includes additional details:
{
"error": "PERMISSION_DENIED",
"message": "No SELECT permission on table 'main.orders' for role 'viewer'.",
"correlation_id": "req_abc123",
"details": {
"table": "main.orders",
"operation": "select",
"role": "viewer",
"evaluated_permissions": ["view_own_orders"]
}
}cURL Examples
# SELECT query
curl -X POST http://localhost:3001/data \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"sql": "SELECT \"id\", \"amount\", \"status\" FROM \"main\".\"orders\" LIMIT $1",
"params": [10],
"method": "all"
}'
# INSERT query
curl -X POST http://localhost:3001/data \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"sql": "INSERT INTO \"main\".\"orders\" (\"amount\", \"status\") VALUES ($1, $2) RETURNING *",
"params": [250, "draft"],
"method": "run"
}'
# COUNT query
curl -X POST http://localhost:3001/data \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"sql": "SELECT count(*) AS \"count\" FROM \"main\".\"orders\" WHERE \"status\" = $1",
"params": ["active"],
"method": "get"
}'