Using v1.x? See README_v1.md for v1.x documentation.
The Data API Client is a lightweight wrapper that makes using the Amazon Aurora Serverless Data API with your favorite ORMs and query builders incredibly easy. It ships with drop-in compatibility layers for the mysql2 and pg drivers, plus dedicated adapters for Knex and Prisma. Point Drizzle, Kysely, Knex, or Prisma at the Data API and keep writing the queries you already know, with no changes to your models or query code.
Prefer raw SQL? The client gives you a clean, DocumentClient-style interface and handles the tedious part for you. The Data API makes you annotate every field value with its type, both going in and coming back, which gets old fast. This library maps native JavaScript types to the Data API's format and back automatically.
Either way, you get clean transactions and automatic retry logic for scale-to-zero clusters built in.
For more information about the Aurora Serverless Data API, you can review the official documentation.
- Prisma support: the
data-api-client/compat/prismaadapter lets Prisma Client run on the Data API for both PostgreSQL and MySQL.createPrismaPgAdapter()andcreatePrismaMySQLAdapter()return a Prisma 7 driver adapter you hand straight tonew PrismaClient({ adapter })- The full Prisma Client query API is covered for both engines: CRUD, aggregation and
groupBy, the filter operators, pagination and cursors,select/include/omit, nested writes, relation filters, atomic number updates, JSON,Decimal/BigInt/DateTime, PostgreSQL scalar arrays, raw queries, and transactions - Transactions map to the native Data API lifecycle; nested savepoints are rejected, same as the other layers
- Migrations run through Prisma's schema engine, which needs a direct connection the Data API doesn't provide. Point it at your Aurora cluster endpoint (the same split Neon and PlanetScale use) or generate the SQL offline. See the Prisma section for details.
@prisma/driver-adapter-utilsis an optional peer dependency
See the Prisma section for usage.
- Knex support:
data-api-client/compat/knex(createKnexMySQLClient()/createKnexPgClient()) runs Knex on the Data API for MySQL and PostgreSQL, including transactions (nested transactions rejected, since the Data API has noSAVEPOINTprimitive).knexis an optional peer dependency. See the Knex section. - Fixed two latent compatibility-layer bugs that also affected non-Knex callers: pg parameter binding via the config-object callback form, and mysql2 callback parsing
- Dependency updates, including a raised
@aws-sdk/client-rds-datapeer dependency floor
- mysql2 and pg compatibility layers: drop-in replacements for those drivers, with connection pools, transactions, and both Promise and callback APIs
- Automatic retry logic for Aurora Serverless scale-to-zero wake-ups (
DatabaseResumingExceptionand transient connection errors), configurable and enabled by default - Verified ORM support for Drizzle and Kysely
- Migrated to AWS SDK v3 for smaller bundles and better tree-shaking
- Full TypeScript implementation with comprehensive type definitions
- PostgreSQL array results automatically converted to native JavaScript arrays
- Comprehensive PostgreSQL data type coverage: numeric, string, boolean, date/time,
BYTEA, JSON/JSONB, UUID, network (INET/CIDR), range, and arrays of all supported types - Inline (
::type) and parameter-based type casting, with more informative error messages
The Data API Client makes working with the Aurora Serverless Data API super simple. Import and instantiate the library with basic configuration information, then use the query() method to manage your workflows. Below are some examples.
// Import and instantiate data-api-client with secret and cluster
import dataApiClient from 'data-api-client'
const data = dataApiClient({
secretArn: 'arn:aws:secretsmanager:us-east-1:XXXXXXXXXXXX:secret:mySecret',
resourceArn: 'arn:aws:rds:us-east-1:XXXXXXXXXXXX:cluster:my-cluster-name',
database: 'myDatabase', // default database
engine: 'pg' // or 'mysql'
})
/*** Assuming we're in an async function ***/
// Simple SELECT
let result = await data.query(`SELECT * FROM myTable`)
// {
// records: [
// { id: 1, name: 'Alice', age: null },
// { id: 2, name: 'Mike', age: 52 },
// { id: 3, name: 'Carol', age: 50 }
// ]
// }
// SELECT with named parameters
let resultParams = await data.query(`SELECT * FROM myTable WHERE id = :id`, { id: 2 })
// { records: [ { id: 2, name: 'Mike', age: 52 } ] }
// INSERT with named parameters (PostgreSQL with RETURNING)
let insert = await data.query(`INSERT INTO myTable (name, age, has_curls) VALUES(:name, :age, :curls) RETURNING id`, {
name: 'Greg',
age: 18,
curls: false
})
// BATCH INSERT with named parameters
let batchInsert = await data.query(`INSERT INTO myTable (name, age, has_curls) VALUES(:name, :age, :curls)`, [
[{ name: 'Marcia', age: 17, curls: false }],
[{ name: 'Peter', age: 15, curls: false }],
[{ name: 'Jan', age: 15, curls: false }],
[{ name: 'Cindy', age: 12, curls: true }],
[{ name: 'Bobby', age: 12, curls: false }]
])
// Update with named parameters
let update = await data.query(`UPDATE myTable SET age = :age WHERE id = :id`, { age: 13, id: 5 })
// Delete with named parameters
let remove = await data.query(
`DELETE FROM myTable WHERE name = :name`,
{ name: 'Jan' } // Sorry Jan :(
)
// PostgreSQL with automatic JSONB casting for plain objects
let pgExample = await data.query(`INSERT INTO users (id, email, metadata) VALUES(:id, :email, :metadata)`, [
{ name: 'id', value: '550e8400-e29b-41d4-a716-446655440000', cast: 'uuid' },
{ name: 'email', value: 'user@example.com' },
{ name: 'metadata', value: { role: 'admin', permissions: ['read', 'write'] } } // Automatically cast as JSONB!
])
// PostgreSQL array result (automatically converted to native JavaScript array)
let arrayResult = await data.query(`SELECT tags FROM products WHERE id = :id`, { id: 123 })
// { records: [ { tags: ['new', 'featured', 'sale'] } ] }The Data API requires you to specify data types when passing in parameters. The basic INSERT example above would look like this using the native AWS SDK v3:
import { RDSDataClient, ExecuteStatementCommand } from '@aws-sdk/client-rds-data'
const client = new RDSDataClient()
/*** Assuming we're in an async function ***/
// INSERT with named parameters
let insert = await client.send(
new ExecuteStatementCommand({
secretArn: 'arn:aws:secretsmanager:us-east-1:XXXXXXXXXXXX:secret:mySecret',
resourceArn: 'arn:aws:rds:us-east-1:XXXXXXXXXXXX:cluster:my-cluster-name',
database: 'myDatabase',
sql: 'INSERT INTO myTable (name, age, has_curls) VALUES(:name, :age, :curls)',
parameters: [
{ name: 'name', value: { stringValue: 'Cousin Oliver' } },
{ name: 'age', value: { longValue: 10 } },
{ name: 'curls', value: { booleanValue: false } }
]
})
)Specifying all of those data types in the parameters is a bit clunky. In addition to requiring types for parameters, it also returns each field as an object with its value assigned to a key that represents its data type, like this:
{
// id field
longValue: 9
},
{
// name field
stringValue: 'Cousin Oliver'
},
{
// age field
longValue: 10
},
{
// has_curls field
booleanValue: false
}Not only are there no column names, but you have to pull the value from the data type field. And if you're using PostgreSQL arrays, you get a complex nested structure:
{
// tags field (PostgreSQL array)
arrayValue: {
stringValues: ['admin', 'editor', 'viewer']
}
}Lots of extra work that the Data API Client handles automatically for you, converting arrays to native JavaScript arrays and providing clean, usable data. 😀
The AWS Data API offers a built-in JSON format option via the formatRecordsAs: 'JSON' parameter. While this simplifies basic result parsing, the Data API Client provides significantly more value:
Type Fidelity: AWS's JSON format converts everything to basic JSON types, losing database-specific type information. The Data API Client preserves PostgreSQL-specific types (UUID, MACADDR, range types, etc.) using columnMetadata.typeName for intelligent type handling.
Advanced Type Conversion:
- PostgreSQL arrays: Automatically flattens complex
arrayValuestructures to native JavaScript arrays - Binary data: Converts
Uint8Arrayto Node.jsBufferobjects - JSON columns: Auto-parses JSON strings to objects
- Date handling: Configurable deserialization with
deserializeDateandtreatAsLocalDateoptions - MySQL YEAR type: Converts strings to integers automatically
Flexible Output Formats: AWS JSON only returns objects. The Data API Client lets you choose between object format (hydrateColumnNames: true) for easy access by name, or array format (hydrateColumnNames: false) for better performance when column names aren't needed.
Richer Result Information: Beyond just formatted records, you get numberOfRecordsUpdated, insertId, columnMetadata (optional), and batch updateResults for comprehensive operation feedback.
No Additional Limitations: AWS's JSON support requires unique column names and has a 10MB response limit. The Data API Client works with any column configuration and imposes no additional size restrictions.
AWS's JSON support is a basic convenience feature. The Data API Client provides true type intelligence, format flexibility, and seamless handling of complex PostgreSQL features that the native Data API doesn't support well.
npm i data-api-client
The library has AWS SDK v3's @aws-sdk/client-rds-data as an optional peer dependency. In AWS Lambda, the SDK is provided by the runtime. For local development or other environments, install it separately:
npm i @aws-sdk/client-rds-data
For more information on enabling Data API, see Enabling Data API.
Below is a table containing all of the possible configuration options for the data-api-client. Additional details are provided throughout the documentation.
| Property | Type | Description | Default |
|---|---|---|---|
| client | RDSDataClient |
A custom @aws-sdk/client-rds-data instance (for X-Ray tracing, custom config, etc.) |
|
| resourceArn | string |
The ARN of your Aurora Serverless Cluster. This value is required, but can be overridden when querying. | |
| secretArn | string |
The ARN of the secret associated with your database credentials. This is required, but can be overridden when querying. | |
| database | string |
Optional default database to use with queries. Can be overridden when querying. | |
| engine | mysql or pg |
The type of database engine you're connecting to (MySQL or Postgres). | pg |
| hydrateColumnNames | boolean |
When true, results will be returned as objects with column names as keys. If false, results will be returned as an array of values. |
true |
| namedPlaceholders | boolean |
Enable named placeholders (:name syntax) for mysql2 compatibility layer. When true, parameters use object format. Only applies to mysql2 compat layer. |
false |
| options | object |
An optional configuration object that is passed directly into the RDSDataClient constructor. See AWS SDK docs for available options. | {} |
| formatOptions | object |
Formatting options to auto parse dates and coerce native JavaScript date objects to supported date formats. Valid keys are deserializeDate and treatAsLocalDate. Both accept boolean values. |
deserializeDate: true, treatAsLocalDate: false |
| retryOptions | object |
Configuration for automatic retry logic. Valid keys are enabled (boolean), maxRetries (number), and retryableErrors (string array). |
enabled: true, maxRetries: 9 |
Version 2.1 introduced built-in retry logic to handle Aurora Serverless scale-to-zero cluster wake-ups automatically. When your cluster is paused and needs to resume, the client will automatically retry your queries with optimized delays.
Features:
- Smart Error Detection: Automatically detects
DatabaseResumingExceptionand connection errors - Strategy-Based Retries: Different retry strategies based on error type:
- DatabaseResumingException: Up to 10 attempts with progressive delays (0s, 2s, 5s, 10s, 15s, 20s, 25s, 30s, 35s, 40s)
- Connection errors: 3 quick retries with exponential backoff (0s, 2s, 4s)
- Enabled by Default: Works automatically without any configuration
- Configurable: Customize retry behavior per your needs
Configuration:
const data = dataApiClient({
secretArn: 'arn:...',
resourceArn: 'arn:...',
database: 'myDatabase',
retryOptions: {
enabled: true, // Enable/disable retries (default: true)
maxRetries: 9, // Maximum retry attempts (default: 9 for up to 40s total)
retryableErrors: [] // Additional error patterns to retry (optional)
}
})Disable retries (not recommended for scale-to-zero clusters):
const data = dataApiClient({
secretArn: 'arn:...',
resourceArn: 'arn:...',
retryOptions: { enabled: false }
})The retry logic works seamlessly across all operations: queries, transactions, batch operations, and compatibility layer methods.
It is recommended to enable connection reuse as this dramatically decreases the latency of subsequent calls to the AWS API. This can be done by setting an environment variable AWS_NODEJS_CONNECTION_REUSE_ENABLED=1. For more information see the AWS SDK documentation.
The Data API Client wraps the RDSDataClient Class, providing you with a number of convenience features to make your workflow easier. The module also exposes all the standard RDSDataClient methods with your default configuration information already merged in. 😉
To use the Data API Client, import the module and instantiate it with your Configuration options. If you are using it with AWS Lambda, require it OUTSIDE your main handler function. This will allow you to reuse the initialized module on subsequent invocations.
// Import and instantiate data-api-client with secret and cluster arns
import dataApiClient from 'data-api-client'
const data = dataApiClient({
secretArn: 'arn:aws:secretsmanager:us-east-1:XXXXXXXXXXXX:secret:mySecret',
resourceArn: 'arn:aws:rds:us-east-1:XXXXXXXXXXXX:cluster:my-cluster-name',
database: 'myDatabase', // set a default database
engine: 'pg' // specify 'pg' for PostgreSQL or 'mysql' for MySQL
})Once initialized, running a query is super simple. Use the query() method and pass in your SQL statement:
let result = await data.query(`SELECT * FROM myTable`)By default, this will return your rows as an array of objects with column names as property names:
;[
{ id: 1, name: 'Alice', age: null },
{ id: 2, name: 'Mike', age: 52 },
{ id: 3, name: 'Carol', age: 50 }
]To query with parameters, you can use named parameters in your SQL, and then provide an object containing your parameters as the second argument to the query() method:
let result = await data.query(
`
SELECT * FROM myTable WHERE id = :id AND created > :createDate`,
{ id: 2, createDate: '2019-06-01' }
)The Data API Client will automatically convert your parameters into the correct Data API parameter format using native JavaScript types. If you prefer more control over the data type, you can use the extended parameter format:
let result = await data.query(`SELECT * FROM myTable WHERE id = :id AND created > :createDate`, [
// An array of objects is totally cool, too. We'll merge them for you.
{ id: 2 },
// Extended format for more control
{ name: 'createDate', value: '2019-06-01' }
])If you want even more control, you can pass in an object as the first parameter. This will allow you to add additional configuration options and override defaults as well.
let result = await data.query({
sql: `SELECT * FROM myTable WHERE id = :id`,
parameters: [{ id: 2 }], // or just { id: 2 }
database: 'someOtherDatabase', // override default database
continueAfterTimeout: true, // RDSDataService config option (non-batch only)
includeResultMetadata: true, // RDSDataService config option (non-batch only)
hydrateColumnNames: false, // Returns each record as an arrays of values
transactionId: 'AQC5SRDIm...ZHXP/WORU=' // RDSDataService config option
})Sometimes you might want to have dynamic identifiers in your SQL statements. Unfortunately, the native Data API doesn't support this, but the Data API Client does! Use a double colon (::) prefix to create named identifiers and you can do cool things like this:
let result = await data.query(`SELECT ::fields FROM ::table WHERE id > :id`, {
fields: ['id', 'name', 'created'],
table: 'table_' + someScaryUserInput, // someScaryUserInput = 123abc
id: 1
})Which will produce a query like this for PostgreSQL:
SELECT "id", "name", "created" FROM "table_123abc" WHERE id > :idOr for MySQL:
SELECT `id`, `name`, `created` FROM `table_123abc` WHERE id > :idYou'll notice that we leave the named parameters alone. Anything that Data API and the native SDK currently handles, we defer to them.
The Aurora Data API can sometimes give you trouble with certain data types, such as uuid or jsonb in PostgreSQL, unless you explicitly cast them. While you can certainly do this manually in your SQL string using PostgreSQL's :: cast syntax, the Data API Client offers an easy way to handle this for you using the parameter cast property.
New in v2.x: The Data API Client now automatically detects and casts plain JavaScript objects as JSONB in PostgreSQL queries. This eliminates the need for manual JSON.stringify() or explicit ::jsonb casts in most cases:
// Plain JavaScript objects are automatically cast as JSONB
const metadata = { role: 'admin', permissions: ['read', 'write'], score: 95.5 }
await data.query('INSERT INTO users (email, metadata) VALUES (:email, :metadata)', {
email: 'user@example.com',
metadata: metadata // Automatically serialized and cast as ::jsonb
})
// Works with nested objects too
const complexData = {
user: { name: 'Alice', age: 30 },
tags: ['admin', 'editor'],
settings: { theme: 'dark', notifications: true }
}
await data.query('INSERT INTO products (name, data) VALUES (:name, :data)', {
name: 'Product A',
data: complexData // Automatically handled
})How it works:
- Plain JavaScript objects (not Buffers, Dates, Arrays, or Data API objects) are automatically detected
- The object is serialized to a JSON string
- A
::jsonbcast is automatically appended to the parameter in PostgreSQL queries - A
JSONtypeHint is provided to the Data API for proper handling
When automatic casting applies:
- ✅ Plain objects:
{ key: 'value' } - ✅ Nested objects:
{ user: { name: 'Alice' } } - ❌ Buffers:
Buffer.from('data') - ❌ Dates:
new Date() - ❌ Arrays:
[1, 2, 3] - ❌ Already-formatted Data API objects:
{ stringValue: 'text' }
Explicit casting still supported:
You can still use explicit casts when needed (e.g., for UUID, custom types, or to override automatic behavior):
PostgreSQL inline casting:
const result = await data.query('INSERT INTO users(id, email, metadata) VALUES(:id, :email, :metadata::jsonb)', {
id: newUserId,
email: email,
metadata: JSON.stringify(userMetadata) // explicit ::jsonb in SQL
})Parameter-based casting:
const result = await data.query(
'INSERT INTO users(id, email, full_name, metadata) VALUES(:id, :email, :fullName, :metadata)',
[
{
name: 'id',
value: newUserId,
cast: 'uuid'
},
{
name: 'email',
value: email
},
{
name: 'fullName',
value: fullName
},
{
name: 'metadata',
value: JSON.stringify(userMetadata),
cast: 'jsonb'
}
]
)Note: Explicit casts (inline ::type or parameter cast property) always take precedence over automatic casting.
The Data API provides a batchExecuteStatement method that allows you to execute a prepared statement multiple times using different parameter sets. This is only allowed for INSERT, UPDATE and DELETE queries, but is much more efficient than issuing multiple executeStatement calls. The Data API Client handles the switching for you based on how you send in your parameters.
To issue a batch query, use the query() method (either by passing an object or using the two arity form), and provide multiple parameter sets as nested arrays. For example, if you wanted to update multiple records at once, your query might look like this:
let result = await data.query(`UPDATE myTable SET name = :newName WHERE id = :id`, [
[{ id: 1, newName: 'Alice Franklin' }],
[{ id: 7, newName: 'Jan Glass' }]
])You can also use named identifiers in batch queries, which will update and escape your SQL statement. ONLY parameters from the first parameter set will be used to update the query. Subsequent parameter sets will only update named parameters supported by the Data API.
Whenever a batch query is executed, it returns an updateResults field. Other than for INSERT statements, however, there is no useful feedback provided by this field.
The Data API returns a generatedFields array that contains the value of auto-incrementing primary keys. If this value is returned, the Data API Client will parse this and return it as the insertId. This also works for batch queries as well.
For PostgreSQL, use the RETURNING clause to get generated values:
let result = await data.query(`INSERT INTO users (name, email) VALUES (:name, :email) RETURNING id`, {
name: 'Alice',
email: 'alice@example.com'
})
// result.records[0].id contains the generated IDTransaction support in the Data API Client has been dramatically simplified. Start a new transaction using the transaction() method, and then chain queries using the query() method. The query() method supports all standard query options. Alternatively, you can specify a function as the only argument in a query() method call and return the arguments as an array of values. The function receives two arguments, the result of the last query executed, and an array containing all the previous query results. This is useful if you need values from a previous query as part of your transaction.
You can specify an optional rollback() method in the chain. This will receive the error object and the transactionStatus object, allowing you to add additional logging or perform some other action. Call the commit() method when you are ready to execute the queries.
let results = await data
.transaction()
.query('INSERT INTO myTable (name) VALUES(:name)', { name: 'Tiger' })
.query('UPDATE myTable SET age = :age WHERE name = :name', { age: 4, name: 'Tiger' })
.rollback((e, status) => {
/* do something with the error */
}) // optional
.commit() // execute the queriesWith a function to get the insertId from the previous query:
let results = await data
.transaction()
.query('INSERT INTO myTable (name) VALUES(:name) RETURNING id', { name: 'Tiger' })
.query((r) => ['UPDATE myTable SET age = :age WHERE id = :id', { age: 4, id: r.records[0].id }])
.rollback((e, status) => {
/* do something with the error */
}) // optional
.commit() // execute the queriesTransactions work with batch queries, too! 👊
By default, the transaction() method will use the resourceArn, secretArn and database values you set at initialization. Any or all of these values can be overwritten by passing an object into the transaction() method. Since transactions are for a specific database, you can't overwrite their values when chaining queries. You can, however, overwrite the includeResultMetadata and hydrateColumnNames settings per query.
The Data API Client exposes the five RDSDataClient command methods. These are:
batchExecuteStatementbeginTransactioncommitTransactionexecuteStatementrollbackTransaction
The default configuration information (resourceArn, secretArn, and database) are merged with your supplied parameters, so supplying those values are optional.
let result = await data.executeStatement({
sql: `SELECT * FROM myTable WHERE id = :id`,
parameters: [{ name: 'id', value: { longValue: 1 } }],
transactionId: 'AQC5SRDIm...ZHXP/WORU='
})data-api-client allows for introducing a custom RDSDataClient instance as a parameter. This parameter is optional. If not present, data-api-client will create a default instance.
import { RDSDataClient } from '@aws-sdk/client-rds-data'
import dataApiClient from 'data-api-client'
// Create a custom client instance
const rdsClient = new RDSDataClient({
region: 'us-east-1'
// other configuration options
})
// Instantiate data-api-client with the custom client
const data = dataApiClient({
client: rdsClient,
secretArn: 'arn:aws:secretsmanager:us-east-1:XXXXXXXXXXXX:secret:mySecret',
resourceArn: 'arn:aws:rds:us-east-1:XXXXXXXXXXXX:cluster:my-cluster-name'
})Custom client parameter allows you to introduce X-Ray tracing:
import { RDSDataClient } from '@aws-sdk/client-rds-data'
import { captureAWSv3Client } from 'aws-xray-sdk-core'
import dataApiClient from 'data-api-client'
const rdsClient = captureAWSv3Client(new RDSDataClient({ region: 'us-east-1' }))
const data = dataApiClient({
client: rdsClient,
secretArn: 'arn:aws:secretsmanager:us-east-1:XXXXXXXXXXXX:secret:mySecret',
resourceArn: 'arn:aws:rds:us-east-1:XXXXXXXXXXXX:cluster:my-cluster-name'
})Version 2.1 introduced compatibility layers that allow you to use the Data API Client as a drop-in replacement for popular database libraries. This makes it easy to migrate existing applications or use ORMs without modification.
Use the Data API Client as a replacement for mysql2/promise:
import { createMySQLConnection, createMySQLPool } from 'data-api-client/compat/mysql2'
// Create a connection
const connection = createMySQLConnection({
resourceArn: 'arn:aws:rds:us-east-1:XXXXXXXXXXXX:cluster:my-cluster',
secretArn: 'arn:aws:secretsmanager:us-east-1:XXXXXXXXXXXX:secret:mySecret',
database: 'myDatabase'
})
// Use like mysql2/promise with positional placeholders
const [rows, fields] = await connection.query('SELECT * FROM users WHERE id = ?', [123])
await connection.execute('INSERT INTO users (name, email) VALUES (?, ?)', ['Alice', 'alice@example.com'])
// Note: connection.end() is optional - it's a no-op for Data API (no connection to close)
// Create a pool for connection pooling
const pool = createMySQLPool({
resourceArn: 'arn:...',
secretArn: 'arn:...',
database: 'myDatabase'
})
// Get connection from pool
pool.getConnection((err, connection) => {
if (err) throw err
connection.query('SELECT * FROM users', (err, results) => {
connection.release() // Optional - no-op for Data API
// Handle results
})
})
// Or use promises
const connection = await pool.getConnection()
const [rows] = await connection.query('SELECT * FROM users')
connection.release() // Optional - no-op for Data APIThe mysql2 compatibility layer supports named placeholders (:name syntax), matching the behavior of the native mysql2 library's namedPlaceholders option:
import { createMySQLConnection, createMySQLPool } from 'data-api-client/compat/mysql2'
// Create a connection with namedPlaceholders enabled
const connection = createMySQLConnection({
resourceArn: 'arn:aws:rds:us-east-1:XXXXXXXXXXXX:cluster:my-cluster',
secretArn: 'arn:aws:secretsmanager:us-east-1:XXXXXXXXXXXX:secret:mySecret',
database: 'myDatabase',
namedPlaceholders: true // Enable named placeholders
})
// Use named placeholders with object parameters
const [users] = await connection.query('SELECT * FROM users WHERE name = :name AND age > :age', {
name: 'Alice',
age: 25
})
// INSERT with named placeholders
await connection.query('INSERT INTO users (name, email, active) VALUES (:name, :email, :active)', {
name: 'Bob',
email: 'bob@example.com',
active: true
})
// UPDATE with named placeholders
await connection.query('UPDATE users SET age = :newAge WHERE id = :id', { id: 123, newAge: 30 })
// Named placeholders work with transactions
await connection.beginTransaction()
try {
await connection.query('INSERT INTO orders (user_id, total) VALUES (:userId, :total)', { userId: 123, total: 99.99 })
await connection.query('UPDATE users SET last_order = NOW() WHERE id = :userId', { userId: 123 })
await connection.commit()
} catch (err) {
await connection.rollback()
}
// Named placeholders also work with pools
const pool = createMySQLPool({
resourceArn: 'arn:...',
secretArn: 'arn:...',
database: 'myDatabase',
namedPlaceholders: true
})
const [results] = await pool.query('SELECT * FROM products WHERE category = :category AND price < :maxPrice', {
category: 'electronics',
maxPrice: 500
})Named Placeholders Features:
- Use
:paramNamesyntax in SQL (colon followed by identifier) - Pass parameters as objects:
{ paramName: value } - Same parameter can be referenced multiple times in the query
- Works with all query types (SELECT, INSERT, UPDATE, DELETE)
- Fully compatible with transactions, pools, and callbacks
- Backward compatible: positional
?placeholders still work whennamedPlaceholdersis disabled (default)
Query-Level namedPlaceholders:
You can also enable or disable named placeholders on a per-query basis, which overrides the connection-level setting:
// Connection WITHOUT namedPlaceholders at config level
const connection = createMySQLConnection({
resourceArn: 'arn:...',
secretArn: 'arn:...',
database: 'myDatabase'
// namedPlaceholders NOT set (defaults to false)
})
// Enable namedPlaceholders for specific queries
const [rows] = await connection.query(
{
sql: 'SELECT * FROM users WHERE username = :username AND age > :minAge',
namedPlaceholders: true // Enable for this query only
},
{ username: 'john_doe', minAge: 25 }
)
// Or explicitly disable for a specific query (when connection has it enabled)
const [rows2] = await connection.query(
{
sql: 'SELECT * FROM users WHERE id = ?',
namedPlaceholders: false // Use positional placeholders for this query
},
[123]
)This allows you to:
- Use named placeholders in specific queries without enabling it globally
- Mix named and positional placeholders in different queries
- Override connection-level settings when needed
Use the Data API Client as a replacement for pg (node-postgres):
import { createPgClient, createPgPool } from 'data-api-client/compat/pg'
// Create a client
const client = createPgClient({
resourceArn: 'arn:aws:rds:us-east-1:XXXXXXXXXXXX:cluster:my-cluster',
secretArn: 'arn:aws:secretsmanager:us-east-1:XXXXXXXXXXXX:secret:mySecret',
database: 'myDatabase'
})
// Note: client.connect() is optional - it's a no-op for Data API (no connection needed)
await client.connect() // Optional
// Use like pg
const result = await client.query('SELECT * FROM users WHERE id = $1', [123])
console.log(result.rows)
// With callback style
client.query('SELECT * FROM users', (err, result) => {
console.log(result.rows)
})
// Note: client.end() is optional - it's a no-op for Data API (no connection to close)
await client.end() // Optional
// Create a pool
const pool = createPgPool({
resourceArn: 'arn:...',
secretArn: 'arn:...',
database: 'myDatabase'
})
const result = await pool.query('SELECT * FROM users WHERE id = $1', [123])The compatibility layers work seamlessly with popular ORMs:
MySQL with Drizzle:
import { drizzle } from 'drizzle-orm/mysql2'
import { createMySQLPool } from 'data-api-client/compat/mysql2'
const pool = createMySQLPool({
resourceArn: 'arn:...',
secretArn: 'arn:...',
database: 'myDatabase'
})
const db = drizzle(pool as any)
// Use Drizzle normally
const users = await db.select().from(usersTable).where(eq(usersTable.id, 123))PostgreSQL with Drizzle:
import { drizzle } from 'drizzle-orm/node-postgres'
import { createPgClient } from 'data-api-client/compat/pg'
const client = createPgClient({
resourceArn: 'arn:...',
secretArn: 'arn:...',
database: 'myDatabase'
})
// Note: client.connect() is optional (no-op for Data API)
await client.connect() // Optional - can be omitted
const db = drizzle(client as any)
// Use Drizzle normally
const users = await db.select().from(usersTable).where(eq(usersTable.id, 123))MySQL with Kysely:
import { Kysely, MysqlDialect } from 'kysely'
import { createMySQLPool } from 'data-api-client/compat/mysql2'
const pool = createMySQLPool({
resourceArn: 'arn:...',
secretArn: 'arn:...',
database: 'myDatabase'
})
const db = new Kysely<Database>({
dialect: new MysqlDialect({ pool: pool as any })
})
// Use Kysely normally
const users = await db.selectFrom('users').selectAll().where('id', '=', 123).execute()PostgreSQL with Kysely:
import { Kysely, PostgresDialect } from 'kysely'
import { createPgPool } from 'data-api-client/compat/pg'
const pool = createPgPool({
resourceArn: 'arn:...',
secretArn: 'arn:...',
database: 'myDatabase'
})
const db = new Kysely<Database>({
dialect: new PostgresDialect({ pool: pool as any })
})
// Use Kysely normally
const users = await db.selectFrom('users').selectAll().where('id', '=', 123).execute()Knex doesn't accept an injected pool like Drizzle and Kysely. It constructs its own
driver internally, so the data-api-client/compat/knex helpers return a custom Knex
client class wired to the Data API, which you pass as client:
MySQL with Knex:
import knex from 'knex'
import { createKnexMySQLClient } from 'data-api-client/compat/knex'
const db = knex({
client: createKnexMySQLClient({
resourceArn: 'arn:...',
secretArn: 'arn:...',
database: 'myDatabase'
}),
connection: {}
})
// Use Knex normally
const users = await db('users').where('id', 123).select('*')PostgreSQL with Knex:
import knex from 'knex'
import { createKnexPgClient } from 'data-api-client/compat/knex'
const db = knex({
client: createKnexPgClient({
resourceArn: 'arn:...',
secretArn: 'arn:...',
database: 'myDatabase'
}),
connection: {}
})
// Use Knex normally
const id = await db('users').insert({ name: 'Alice' }).returning('id')Note:
knexis an optional peer dependency. Install it alongsidedata-api-clientto use these helpers.
Knex transactions work. The compat layer intercepts the BEGIN/COMMIT/ROLLBACK
SQL that Knex issues and maps it to the Data API transaction lifecycle:
await db.transaction(async (trx) => {
const [userId] = await trx('users').insert({ name: 'Alice' }).returning('id')
await trx('posts').insert({ user_id: userId, title: 'Hello' })
}) // commits on success, rolls back if the callback throwsNested transactions are not supported. They require SQL
SAVEPOINTs, which the RDS Data API has no primitive for, so a nestedtrx.transaction(...)throws. A single top-level transaction works as shown above.
The common query-builder syntax is covered by integration tests for both engines:
selects/distinct/pluck/first, the where family (whereIn, whereNull,
whereBetween, whereExists, whereRaw, and so on), joins, groupBy/having/aggregates,
orderBy/limit/offset, unions, subqueries, CTEs (with), insert/update/del,
returning (PostgreSQL), increment/decrement, and onConflict().merge() upserts.
Streaming via .stream() is not supported, since the Data API has no cursor API.
A drop-in Prisma 7 driver adapter so Prisma Client runs over the Aurora Data API, for both PostgreSQL and MySQL.
Install:
You need @prisma/client and prisma installed in your project. The adapter uses @prisma/driver-adapter-utils, which is an optional peer dependency of data-api-client. Install it alongside:
npm install @prisma/client prisma @prisma/driver-adapter-utilsUsage (PostgreSQL):
import { PrismaClient } from '@prisma/client'
import { createPrismaPgAdapter } from 'data-api-client/compat/prisma'
const adapter = createPrismaPgAdapter({
secretArn: process.env.SECRET_ARN,
resourceArn: process.env.RESOURCE_ARN,
database: 'mydb'
})
const prisma = new PrismaClient({ adapter })Use createPrismaMySQLAdapter for MySQL. It takes the same config shape.
Limitations:
- Nested transactions are not supported. They require SQL
SAVEPOINTs, which the RDS Data API has no primitive for. Top-level interactive transactions (viaprisma.$transaction()) work correctly. - Array parameters: the Data API cannot bind array parameters directly. The Prisma adapter handles this for PostgreSQL native array columns by rewriting array values to
ARRAY[...]constructor syntax automatically. The underlying Data API constraint remains; the rewrite happens in the adapter before the query reaches the wire.
Migrations:
Prisma driver adapters cover the runtime query path only. Prisma's Schema Engine (prisma migrate, db push, db pull) requires a direct database connection URL, which the Data API does not provide. This is the same split every serverless driver has. Neon uses a directUrl, PlanetScale uses a connection string, and driver adapters handle runtime while schema operations need a real connection.
The recommended approach: Aurora Serverless v2 also exposes a standard PostgreSQL/MySQL cluster endpoint. Point Prisma's migration url in prisma.config.ts at that direct Aurora endpoint, exactly like Neon's directUrl pattern, and use the Data API adapter at runtime only. This requires network access to the cluster endpoint (in-VPC CI, a bastion/tunnel/VPN, or a publicly accessible dev cluster).
If direct endpoint access is not available: generate migration SQL offline with prisma migrate diff (no live database connection needed; use schema-to-schema diffs to avoid the shadow-database requirement) and apply it over the Data API adapter.
Benefits of Compatibility Layers:
- Zero code changes when migrating from mysql2 or pg
- Full ORM support (Drizzle, Kysely, Knex, Prisma)
- Automatic retry logic for cluster wake-ups
- Connection pooling simulation (getConnection, release)
- Both Promise and callback APIs supported
- No-op connection management:
connect(),end(), andrelease()are optional since the Data API is connectionless - they're included only for backward compatibility with existing code
One of the most powerful features in v2.0 is automatic PostgreSQL array handling. While the Data API has limitations with array parameters, array results are fully supported and automatically converted to native JavaScript arrays.
When you query PostgreSQL arrays, the Data API Client automatically converts them to native JavaScript arrays:
// Query returns PostgreSQL array
let result = await data.query(`SELECT tags FROM products WHERE id = :id`, { id: 123 })
// Automatic conversion to JavaScript array
// result.records[0].tags = ['new', 'featured', 'sale']Supported Array Types:
- Integer arrays:
INT[],SMALLINT[],BIGINT[] - Float arrays:
REAL[],DOUBLE PRECISION[],NUMERIC[] - String arrays:
TEXT[],VARCHAR[] - Boolean arrays:
BOOL[] - Date/Time arrays:
DATE[],TIMESTAMP[] - Other types:
UUID[],JSON[],JSONB[]
The RDS Data API does not support binding array parameters directly. You'll need to use one of these workarounds:
1. CSV string with string_to_array() (for integer arrays):
await data.query("INSERT INTO products (tags) VALUES (string_to_array(:csv, ',')::int[])", {
csv: '1,2,3'
})2. PostgreSQL array literal syntax:
await data.query('INSERT INTO products (tags) VALUES (:literal::text[])', {
literal: '{"admin","editor","viewer"}'
})3. ARRAY[] constructor with individual parameters:
await data.query('INSERT INTO products (tags) VALUES (ARRAY[:tag1, :tag2, :tag3])', {
tag1: 'blue',
tag2: 'sale',
tag3: 'featured'
})Despite these input limitations, all array results are automatically converted to native JavaScript arrays, making it easy to work with PostgreSQL array data in your application. NULL elements inside arrays round-trip correctly. For example, '{1,NULL,3}'::int[] deserializes to [1, null, 3].
Version 2.0 introduced comprehensive support for PostgreSQL data types:
SMALLINT,INT,BIGINT- Integer types of various sizesDECIMAL,NUMERIC- Exact numeric types with precisionREAL,DOUBLE PRECISION- Floating-point types
await data.query('INSERT INTO products (price, quantity) VALUES (:price, :quantity)', {
price: 19.99,
quantity: 100
})CHAR,VARCHAR,TEXT- Character types- Full Unicode support
await data.query('INSERT INTO posts (title, content) VALUES (:title, :content)', {
title: 'Hello 世界 🌍',
content: 'A very long text...'
})await data.query('INSERT INTO users (active) VALUES (:active)', { active: true })DATE- Calendar dateTIME,TIME WITH TIME ZONE- Time of dayTIMESTAMP,TIMESTAMP WITH TIME ZONE- Date and time
await data.query('INSERT INTO events (event_date, event_time) VALUES (:date, :time)', {
date: '2024-12-25',
time: new Date()
})const binaryData = Buffer.from('Binary content', 'utf-8')
await data.query('INSERT INTO files (content) VALUES (:content)', { content: binaryData })Automatic JSONB Casting (New in v2.x):
The Data API Client now automatically detects and casts plain JavaScript objects as JSONB in PostgreSQL:
// Automatic JSONB casting - no manual JSON.stringify() needed!
const metadata = { role: 'admin', permissions: ['read', 'write'] }
await data.query('INSERT INTO users (metadata) VALUES (:metadata)', {
metadata: metadata // Automatically serialized and cast as ::jsonb
})
// Works with nested objects
const complexData = { user: { name: 'Alice' }, settings: { theme: 'dark' } }
await data.query('INSERT INTO users (data) VALUES (:data)', {
data: complexData // Automatically handled
})
// Query result
let result = await data.query('SELECT metadata FROM users WHERE id = :id', { id: 1 })
const parsed = JSON.parse(result.records[0].metadata)Manual casting (still supported):
You can still use explicit casting when needed:
const metadata = { role: 'admin', permissions: ['read', 'write'] }
await data.query('INSERT INTO users (metadata) VALUES (:metadata::jsonb)', {
metadata: JSON.stringify(metadata) // Manual approach
})await data.query('INSERT INTO sessions (session_id) VALUES (:id::uuid)', {
id: '550e8400-e29b-41d4-a716-446655440000'
})
// Or with explicit cast parameter
await data.query('INSERT INTO sessions (session_id) VALUES (:id)', [
{ name: 'id', value: '550e8400-e29b-41d4-a716-446655440000', cast: 'uuid' }
])INET- IPv4 or IPv6 host addressCIDR- IPv4 or IPv6 network
await data.query('INSERT INTO servers (ip_address, network) VALUES (:ip::inet, :net::cidr)', {
ip: '192.168.1.1',
net: '10.0.0.0/8'
})INT4RANGE,NUMRANGE- Numeric rangesTSTZRANGE- Timestamp ranges
await data.query('INSERT INTO bookings (date_range) VALUES (:range::INT4RANGE)', {
range: '[1,10)'
})Version 2.x is written in TypeScript and provides comprehensive type definitions:
import dataApiClient from 'data-api-client'
import type { DataAPIClientConfig, QueryResult } from 'data-api-client/types'
const config: DataAPIClientConfig = {
secretArn: 'arn:...',
resourceArn: 'arn:...',
database: 'mydb',
engine: 'pg'
}
const client = dataApiClient(config)
interface User {
id: number
name: string
email: string
tags: string[]
}
const result: QueryResult<User> = await client.query<User>('SELECT * FROM users WHERE id = :id', { id: 123 })While the Data API is powerful, there are some limitations to be aware of:
The RDS Data API does not support binding array parameters directly. Attempts to use arrayValue parameters result in ValidationException: Array parameters are not supported. See PostgreSQL Array Support for workarounds.
Despite parameter limitations, array results work great! The Data API Client automatically converts PostgreSQL arrays in query results to native JavaScript arrays.
The Data API returns at most about 1 MB of data per statement, and it has no server-side pagination. There's no cursor or nextToken to fetch the next chunk, so a single query that returns more than ~1 MB fails instead of paging.
The fix is to page the query yourself. Add a LIMIT and walk through the rows, and prefer keyset pagination (a WHERE id > :lastId ORDER BY id LIMIT :pageSize style) over OFFSET, which gets slow on deep pages. If you're using one of the ORM or query-builder layers, you already have this: Drizzle, Kysely, Knex, and Prisma all expose take/skip/cursor, so page the way you normally would and you won't hit the cap.
- MACADDR: Not supported by the Data API
- Multidimensional Arrays: Limited support for arrays with more than one dimension
- Some Range Types: INT8RANGE, DATERANGE, TSRANGE have casting issues
Batch operations don't return numberOfRecordsUpdated for UPDATE/DELETE statements.
To use the Data API you need three things:
- The Data API enabled on your Aurora cluster. It's supported on Aurora Serverless v2 and Aurora provisioned clusters (Aurora Serverless v1 uses the original Data API). You can enable it when you create the cluster or by modifying an existing one.
- A Secrets Manager secret with the database credentials. The Data API reads the secret instead of you passing credentials on every call.
- IAM permissions for your execution environment (covered below).
The AWS console and the list of supported engines change often, so rather than walk through a screenshot tour that goes stale, follow the official docs. They stay current:
- Enabling the RDS Data API
- Authorizing access to the Data API (storing credentials in Secrets Manager and the IAM setup)
- Region and version availability
You'll need your cluster's ARN and the secret's ARN to configure the client. See Configuration Options.
In order to use the Data API, your execution environment requires several IAM permissions. Below are the minimum permissions required. Please Note: The Resource: "*" permission for rds-data is recommended by AWS (see here) because Amazon RDS Data API does not support specifying a resource ARN. The credentials specified in Secrets Manager can be used to restrict access to specific databases.
YAML:
Statement:
- Effect: 'Allow'
Action:
- 'rds-data:ExecuteStatement'
- 'rds-data:BatchExecuteStatement'
- 'rds-data:BeginTransaction'
- 'rds-data:RollbackTransaction'
- 'rds-data:CommitTransaction'
Resource: '*'
- Effect: 'Allow'
Action:
- 'secretsmanager:GetSecretValue'
Resource: 'arn:aws:secretsmanager:{REGION}:{ACCOUNT-ID}:secret:{PATH-TO-SECRET}/*'JSON:
"Statement" : [
{
"Effect": "Allow",
"Action": [
"rds-data:ExecuteStatement",
"rds-data:BatchExecuteStatement",
"rds-data:BeginTransaction",
"rds-data:RollbackTransaction",
"rds-data:CommitTransaction"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [ "secretsmanager:GetSecretValue" ],
"Resource": "arn:aws:secretsmanager:{REGION}:{ACCOUNT-ID}:secret:{PATH-TO-SECRET}/*"
}
]Contributions, ideas and bug reports are welcome and greatly appreciated. Please add issues for suggestions and bug reports or create a pull request. You can also contact me on X: @jeremy_daly or LinkedIn: https://www.linkedin.com/in/jeremydaly/.
