tRPC API to Relational Database
The connection generator wires a tRPC API to a Relational Database project, generating a type-safe tRPC middleware plugin that makes a Prisma client available in your procedure context.
Prerequisites
Section titled “Prerequisites”Before using this generator, ensure you have:
Run the Generator
Section titled “Run the Generator”- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - connection - Fill in the required parameters
- Click
Generate
pnpm nx g @aws/nx-plugin:connectionyarn nx g @aws/nx-plugin:connectionnpx nx g @aws/nx-plugin:connectionbunx nx g @aws/nx-plugin:connectionYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:connection --dry-runyarn nx g @aws/nx-plugin:connection --dry-runnpx nx g @aws/nx-plugin:connection --dry-runbunx nx g @aws/nx-plugin:connection --dry-runSelect your tRPC API project as the source and your relational database project as the target.
Options
Section titled “Options”| Parameter | Type | Default | Description |
|---|---|---|---|
| sourceProject Required | string | - | The source project |
| targetProject Required | string | - | The target project to connect to |
| sourceComponent | string | - | The source component to connect from (component name, path relative to source project root, or generator id). Use '.' to explicitly select the project as the source. |
| targetComponent | string | - | The target component to connect to (component name, path relative to target project root, or generator id). Use '.' to explicitly select the project as the target. |
| preferInstallDependencies | boolean | true | Whether to prefer installing dependencies after the generator runs. Set to false to defer installing when batching multiple generators (an install still runs if needed so subsequent generators can compute the Nx project graph); install once at the end. |
Generator Output
Section titled “Generator Output”The generator creates a middleware file in your tRPC API project:
Directorypackages/api/src
Directorymiddleware
- <db-name>.ts tRPC plugin exposing the Prisma client in procedure context
Additionally, it updates your tRPC API’s dev target to start the database automatically when running locally.
Using the Middleware
Section titled “Using the Middleware”Register the Plugin
Section titled “Register the Plugin”Add the generated plugin to your tRPC router so all procedures using it gain access to the database:
import { t } from './init.js';import { createMyDbPlugin } from './middleware/my-db.js';
export const authenticatedProcedure = t.procedure .concat(createMyDbPlugin());Access the Database in Procedures
Section titled “Access the Database in Procedures”The plugin merges IMyDbContext into your procedure context, making myDb available as an optional property:
import { z } from 'zod';import { authenticatedProcedure } from '../router.js';
export const listUsers = authenticatedProcedure .output(z.array(z.object({ id: z.string(), name: z.string() }))) .query(async ({ ctx }) => { // ctx.myDb is the Prisma client — typed as Awaited<ReturnType<typeof getPrisma>> return await ctx.myDb!.user.findMany(); });MySQL: Disconnect After Each Request
Section titled “MySQL: Disconnect After Each Request”When the target database uses the MySQL engine, the generated middleware wraps opts.next() in a try/finally block that calls $disconnect():
return t.procedure.use(async (opts) => { const myDb = await getPrisma(); try { return await opts.next({ ctx: { ...opts.ctx, myDb } }); } finally { await myDb.$disconnect(); }});This addresses the MySQL adapter holding the Node.js event loop open after a query, which would otherwise prevent Lambda from flushing streaming responses. Disconnecting in finally releases the event loop so the response can complete. See MySQL: API Gateway Streaming Mode for details.
PostgreSQL does not require this — its adapter uses a connection pool configured with allowExitOnIdle: true.
Multiple Databases
Section titled “Multiple Databases”You can connect additional databases by running the generator again with a different target. Each database gets its own plugin and context interface:
export const dbProcedure = t.procedure .concat(createMyDbPlugin()) .concat(createOtherDbPlugin());Infrastructure
Section titled “Infrastructure”To allow your API to connect to the database at runtime, the API Lambda functions must be deployed into the same VPC as the database and granted network and IAM access.
In your application stack, deploy the API into the same VPC as the database, then call allowDefaultPortFrom and grantConnect to open the network path and grant IAM rds-db:connect permission to each Lambda handler:
import { MyDatabase } from ':my-scope/common-constructs';
const db = new MyDatabase(this, 'Db', { vpc, ... });
const api = new MyApi(this, 'Api', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, vpcSubnets: { subnetType: SubnetType.PRIVATE_WITH_EGRESS }, }) .build(),});
Object.entries(api.integrations).forEach(([operation, integration]) => { db.allowDefaultPortFrom(integration.handler, `Allow ${operation} to connect to the database`); db.grantConnect(integration.handler);});Deploy the API Lambda functions into a private subnet with egress, not a private isolated subnet. At runtime, getPrisma() retrieves database connection details from AWS AppConfig, which is a public AWS service endpoint that requires outbound internet access.
Deploy the API into the same VPC as the database, grant it rds-db:connect via additional_iam_policy_statements, and open the network path with a pair of security group rules. The aws_vpc.main and aws_subnet resources are defined in the database deployment guide:
module "my_database" { source = "../../common/terraform/src/app/dbs/my-database" vpc_id = aws_vpc.main.id database_subnet_ids = aws_subnet.database[*].id lambda_subnet_ids = aws_subnet.private[*].id}
module "api" { source = "../../common/terraform/src/app/apis/my-api" enable_vpc = true vpc_id = aws_vpc.main.id subnet_ids = aws_subnet.private[*].id
appconfig_application_id = module.runtime_config_appconfig.application_id appconfig_application_arn = module.runtime_config_appconfig.application_arn
additional_iam_policy_statements = [ { Effect = "Allow" Action = ["rds-db:connect"] Resource = [ "arn:aws:rds-db:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:dbuser:${module.my_database.connect_resource_id}/${module.my_database.database_runtime_user}" ] } ]}
resource "aws_vpc_security_group_ingress_rule" "api_to_database" { description = "Allow the API Lambda functions to connect to the database" security_group_id = module.my_database.security_group_id referenced_security_group_id = module.api.security_group_id from_port = module.my_database.cluster_port to_port = module.my_database.cluster_port ip_protocol = "tcp"}
resource "aws_vpc_security_group_egress_rule" "api_to_database" { description = "Allow outbound traffic from the API Lambda functions to the database" security_group_id = module.api.security_group_id referenced_security_group_id = module.my_database.security_group_id from_port = module.my_database.cluster_port to_port = module.my_database.cluster_port ip_protocol = "tcp"}Deploy the API Lambda functions into private subnets with egress, not private isolated subnets. appconfig_application_id/appconfig_application_arn come from the shared runtime configuration AppConfig application declared once in your root module, not from the database module — passing them sets RUNTIME_CONFIG_APP_ID on the Lambda functions and grants them read access to the application. Include the database namespace when instantiating it so the database module’s runtime configuration entry is deployed:
module "runtime_config_appconfig" { source = "../../common/terraform/src/core/runtime-config/appconfig"
application_name = "my-app-runtime-config" namespaces = ["connection", "agentcore", "database"]}SSL Requirements When Connecting Without RDS Proxy
Section titled “SSL Requirements When Connecting Without RDS Proxy”For direct Aurora cluster connections from Node.js 20 or later Lambda runtimes, load the Amazon RDS CA bundle by setting NODE_EXTRA_CA_CERTS:
const api = new Api(this, 'Api', { integrations: Api.defaultIntegrations(this) .withDefaultOptions({ environment: { NODE_EXTRA_CA_CERTS: '/var/runtime/ca-cert.pem', }, }) .build(),});module "api" { source = "..." ...
environment_variables = { NODE_EXTRA_CA_CERTS = "/var/runtime/ca-cert.pem" }}For more details, see the AWS Lambda SSL/TLS requirements for Amazon RDS connections and the Amazon RDS Proxy TLS documentation. When using RDS Proxy, you do not need to configure the RDS CA bundle in your Lambda function.
Local Development
Section titled “Local Development”The generator configures your tRPC API’s dev target to depend on the database’s dev target, so running:
pnpm nx dev <api-project-name>yarn nx dev <api-project-name>npx nx dev <api-project-name>bunx nx dev <api-project-name>will automatically start the local database alongside your API.