Skip to content

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.

Before using this generator, ensure you have:

  1. A ts#trpc-api project
  2. A ts#rdb project
  1. Install the Nx Console VSCode Plugin if you haven't already
  2. Open the Nx Console in VSCode
  3. Click Generate (UI) in the "Common Nx Commands" section
  4. Search for @aws/nx-plugin - connection
  5. Fill in the required parameters
    • Click Generate

    Select your tRPC API project as the source and your relational database project as the target.

    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.

    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 serve-local target to start the database automatically when running locally.

    Add the generated plugin to your tRPC router so all procedures using it gain access to the database:

    packages/api/src/router.ts
    import { t } from './init.js';
    import { createMyDbPlugin } from './middleware/my-db.js';
    export const authenticatedProcedure = t.procedure
    .concat(createMyDbPlugin());

    The plugin merges IMyDbContext into your procedure context, making myDb available as an optional property:

    packages/api/src/procedures/users.ts
    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();
    });

    When the target database uses the MySQL engine, the generated middleware wraps opts.next() in a try/finally block that calls $disconnect():

    packages/api/src/middleware/my-db.ts
    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.

    You can connect additional databases by running the generator again with a different target. Each database gets its own plugin and context interface:

    packages/api/src/router.ts
    export const dbProcedure = t.procedure
    .concat(createMyDbPlugin())
    .concat(createOtherDbPlugin());

    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:

    packages/infra/src/stacks/application-stack.ts
    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.

    The generator configures your tRPC API’s serve-local target to depend on the database’s serve-local target, so running:

    Terminal window
    pnpm nx serve-local <api-project-name>

    will automatically start the local database alongside your API.