Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ export default defineConfig({
{ label: 'ts#project', link: '/guides/typescript-project' },
{ label: 'ts#infra', link: '/guides/typescript-infrastructure' },
{ label: 'ts#trpc-api', link: '/guides/trpc' },
{ label: 'ts#trpc-api (ECS)', link: '/guides/ecs-trpc' },
{ label: 'ts#smithy-api', link: '/guides/ts-smithy-api' },
{
label: 'ts#react-website',
Expand Down
258 changes: 258 additions & 0 deletions docs/src/content/docs/en/guides/ecs-trpc.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
---
title: ECS tRPC
description: Reference documentation for ECS tRPC
---
import { FileTree } from '@astrojs/starlight/components';
import Link from '@components/link.astro';
import RunGenerator from '@components/run-generator.astro';
import GeneratorParameters from '@components/generator-parameters.astro';
import NxCommands from '@components/nx-commands.astro';
import Snippet from '@components/snippet.astro';

[tRPC](https://trpc.io/) is a framework for building APIs in TypeScript with end-to-end type safety. Using tRPC, updates to API operation inputs and outputs are immediately reflected in client code and are visible in your IDE without the need to rebuild your project.

The ECS tRPC API generator creates a new tRPC API deployed as a containerised [AWS ECS Fargate](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html) service, fronted by an [API Gateway HTTP API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html) via a VPC Link. The generated backend uses [Fastify](https://fastify.dev/) as the HTTP server and includes schema validation using [Zod](https://zod.dev/).

:::tip
ECS Fargate is a good fit when your API has long-running requests, needs consistent low-latency responses without cold starts, or has steady traffic patterns that benefit from always-on compute. If your API is lightweight, has variable or bursty traffic, and can tolerate occasional cold starts, a serverless Lambda deployment may be simpler and more cost-effective. See the <Link path="/guides/trpc">`ts#trpc-api` Lambda guide</Link> for the Lambda-based alternative.
:::

## Usage

### Generate an ECS tRPC API

You can generate a new ECS tRPC API in two ways:

<RunGenerator generator="ts#trpc-api" />

Select `EcsFargate` as the `computeType` when prompted.

### Options

<GeneratorParameters generator="ts#trpc-api" />

## Generator Output

The generator will create the following project structure in the `<directory>/<api-name>` directory:

<FileTree>
- src
- init.ts Backend tRPC initialisation
- server.ts Fastify server entrypoint
- local-server.ts Fastify server entrypoint for local development with CORS enabled
- router.ts tRPC router definition
- index.ts Exports for the router and schemas
- schema Schema definitions using Zod
- echo.ts Example definitions for the input and output of the "echo" procedure
- index.ts Schema barrel export
- procedures Procedures (or operations) exposed by your API
- echo.ts Example procedure
- Dockerfile Dockerfile for building the container image
- tsconfig.json TypeScript configuration
- project.json Project configuration and build targets
</FileTree>

### Infrastructure

<Snippet name="shared-constructs" />

For deploying your API, the following files are generated:

<FileTree>
- packages/common/constructs/src
- app
- ecs-apis
- \<project-name>.ts CDK construct for deploying your ECS API
- core
- ecs
- ecs-api.ts Core CDK construct for ECS Fargate services fronted by API Gateway
- api
- http-api.ts CDK construct for the API Gateway HTTP API
- utils.ts Utilities for the API constructs
</FileTree>

## Implementing your ECS tRPC API

At a high-level, tRPC APIs consist of a router which delegates requests to specific procedures. Each procedure has an input and output, defined as a Zod schema. The ECS variant runs your tRPC router inside a Fastify HTTP server, packaged as a Docker container.

### Schema

<Snippet name="trpc/schema" />

### Router and Procedures

Your tRPC router is defined in `src/router.ts`, which registers all procedures. The Fastify server entry point is in `src/server.ts`, which registers the tRPC router as a Fastify plugin and exposes a `/health` endpoint for load balancer health checks.

<Snippet name="trpc/echo-procedure" />

### Fastify Server

The `src/server.ts` file sets up a [Fastify](https://fastify.dev/) HTTP server that hosts your tRPC router. It registers the tRPC adapter as a Fastify plugin under the `/trpc` prefix and exposes a `/health` endpoint used by the Application Load Balancer for health checks.

```ts
import Fastify from 'fastify';
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
import { appRouter } from './router.js';

const server = Fastify({ logger: true });

void server.register(fastifyTRPCPlugin, {
prefix: '/trpc',
trpcOptions: { router: appRouter },
});

server.get('/health', async () => ({ status: 'ok' }));
```

## Customising your ECS tRPC API

### Errors

<Snippet name="trpc/errors" />

### Organising Your Operations

<Snippet name="trpc/organising-operations" />

### Implementing Custom Middleware

You can add additional values to the context provided to procedures by implementing middleware in `src/init.ts`.

As an example, let's implement some middleware to add request logging:

```ts
import { initTRPC } from '@trpc/server';

export const t = initTRPC.create();

const loggerMiddleware = t.middleware(async (opts) => {
const start = Date.now();
const result = await opts.next();
const duration = Date.now() - start;
console.log(`${opts.type} ${opts.path} - ${duration}ms`);
return result;
});

export const publicProcedure = t.procedure.use(loggerMiddleware);
```

## Deploying your ECS tRPC API

The ECS tRPC API generator creates CDK infrastructure as code. You can use this to deploy your API.

:::note
ECS infrastructure is currently only supported with CDK.
:::

The CDK construct for deploying your API is in the `common/constructs` folder. You can consume this in a CDK application, for example:

```ts {6-8}
import { MyApi } from ':my-scope/common-constructs';

export class ExampleStack extends Stack {
constructor(scope: Construct, id: string) {
// Add the api to your stack
const api = new MyApi(this, 'MyApi');
}
}
```

This sets up your API infrastructure, including:

- A VPC with private subnets
- An ECS Fargate cluster and service running your containerised API
- An internal Application Load Balancer
- An API Gateway HTTP API connected to the ALB via a VPC Link
- CloudWatch log groups for the container and API access logs
- Authentication based on your chosen `auth` method

You can customise the construct by passing optional properties:

```ts
const api = new MyApi(this, 'MyApi', {
cpu: 512,
memoryLimitMiB: 1024,
desiredCount: 2,
environment: {
MY_ENV_VAR: 'my-value',
},
});
```

:::note
If you selected to use `Cognito` authentication, you will need to supply the `identity` property to the API construct:

```ts {9-12}
import { MyApi, UserIdentity } from ':my-scope/common-constructs';

export class ExampleStack extends Stack {
constructor(scope: Construct, id: string) {
const identity = new UserIdentity(this, 'Identity');

const api = new MyApi(this, 'MyApi', {
identity: {
userPool: identity.userPool,
userPoolClient: identity.userPoolClient,
},
});
}
}
```

The `UserIdentity` construct can be generated using the <Link path="/guides/react-website-auth">`ts#react-website-auth` generator</Link>
:::

### Granting Access (IAM Only)

If you selected to use `IAM` authentication, you can grant access to your API:

```ts
api.grantInvokeAccess(myIdentityPool.authenticatedRole);
```

### CORS Configuration

By default, the API allows all origins. You can restrict CORS to specific origins:

```ts
api.restrictCorsTo('https://myapp.com', 'https://staging.myapp.com');
```

### Bundle Target

<Snippet name="ts-bundle" />

## Local Development Server

You can use the `serve` target to run a local development server for your API:

<NxCommands commands={['run @my-scope/my-api:serve']} />

This starts a local development server using `tsx --watch`, which automatically reloads when you make changes to your source code. The entry point for the local server is `src/local-server.ts`.

## Invoking your ECS tRPC API

You can create a tRPC client to invoke your API in a type-safe manner. If you are calling your tRPC API from another backend, you can use the client exported from your API package:

```ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from ':my-scope/my-api';

const client = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'https://my-api-url.example.com/trpc',
}),
],
});

await client.echo.query({ message: 'Hello world!' });
```

If you are calling your API from a React website, consider using the <Link path="guides/connection/react-trpc">Connection</Link> generator to configure the client.

## More Information

For more information about tRPC, please refer to the [tRPC documentation](https://trpc.io/docs).

For more information about ECS Fargate, please refer to the [AWS ECS documentation](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html).
102 changes: 11 additions & 91 deletions docs/src/content/docs/en/guides/trpc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ Select `ServerlessApiGatewayRestApi` (default) as your `computeType` if you woul
The `integrationPattern` option defaults to `isolated`, which creates one Lambda per tRPC procedure. Select `shared` if you would prefer a single shared Lambda handler for the whole API, with optional per-procedure overrides.
:::

:::tip
Select `EcsFargate` as your `computeType` to deploy your API as a containerised ECS Fargate service instead of Lambda. This is a good fit for long-running requests, consistent low-latency without cold starts, or steady traffic patterns. See the <Link path="/guides/ecs-trpc">ECS tRPC guide</Link> for details.
:::

## Generator Output

The generator will create the following project structure in the `<directory>/<api-name>` directory:
Expand Down Expand Up @@ -75,73 +79,17 @@ At a high-level, tRPC APIs consist of a router which delegates requests to speci

### Schema

The `src/schema` directory contains the types that are shared between your client and server code. In this package, these types are defined using [Zod](https://zod.dev/), a TypeScript-first schema declaration and validation library.

An example schema might look as follows:

```ts
import { z } from 'zod';

// Schema definition
export const UserSchema = z.object({
name: z.string(),
height: z.number(),
dateOfBirth: z.string().datetime(),
});

// Corresponding TypeScript type
export type User = z.TypeOf<typeof UserSchema>;
```

Given the above schema, the `User` type is equivalent to the following TypeScript:

```ts
interface User {
name: string;
height: number;
dateOfBirth: string;
}
```

Schemas are shared by both server and client code, providing a single place to update when making changes to the structures used in your API.

Schemas are automatically validated by your tRPC API at runtime, which saves hand-crafting custom validation logic in your backend.

Zod provides powerful utilities to combine or derive schemas such as `.merge`, `.pick`, `.omit` and more. You can find more information on the [Zod documentation website](https://zod.dev/?id=basic-usage).
<Snippet name="trpc/schema" />

### Router and Procedures

Your tRPC router is defined in `src/router.ts`, which registers all procedures. Each procedure defines the expected input, output, and implementation. The Lambda handler entry point is in `src/handler.ts`, which forwards requests to your router.

The sample router generated for you has a single operation, called `echo`:

```ts
import { echo } from './procedures/echo.js';

export const appRouter = router({
echo,
});
```

The example `echo` procedure is generated for you in `src/procedures/echo.ts`:

```ts
export const echo = publicProcedure
.input(EchoInputSchema)
.output(EchoOutputSchema)
.query((opts) => ({ result: opts.input.message }));
```

To break down the above:

- `publicProcedure` defines a public method on the API, including the middleware set up in `src/middleware`. This middleware includes AWS Lambda Powertools integration for logging, tracing and metrics.
- `input` accepts a Zod schema which defines the expected input for the operation. Requests sent for this operation are automatically validated against this schema.
- `output` accepts a Zod schema which defines the expected output for the operation. You will see type errors in your implementation if you don't return an output which conforms to the schema.
- `query` accepts a function which defines the implementation for your API. This implementation receives `opts`, which contains the `input` passed to your operation, as well as other context set up by middleware, available in `opts.ctx`. The function passed to `query` must return an output which conforms to the `output` schema.
<Snippet name="trpc/echo-procedure" />

The use of `query` to define the implementation indicates that the operation is not mutative. Use this to define methods to retrieve data. To implement a mutative operation, use the `mutation` method instead.

If you add a new procedure, make sure you register it by adding it to the router in `src/router.ts`.
:::note
In the Lambda variant, `publicProcedure` also includes the middleware set up in `src/middleware`, which provides AWS Lambda Powertools integration for logging, tracing and metrics.
:::

### Subscriptions (Streaming)

Expand Down Expand Up @@ -191,39 +139,11 @@ The generated infrastructure uses a streaming Lambda handler with `ResponseTrans

### Errors

In your implementation, you can return error responses to clients by throwing a `TRPCError`. These accept a `code` which indicates the type of error, for example:

```ts
throw new TRPCError({
code: 'NOT_FOUND',
message: 'The requested resource could not be found',
});
```
<Snippet name="trpc/errors" />

### Organising Your Operations

As your API grows, you may wish to group related operations together.

You can group operations together using nested routers, for example:

```ts
import { getUser } from './procedures/users/get.js';
import { listUsers } from './procedures/users/list.js';

const appRouter = router({
users: router({
get: getUser,
list: listUsers,
}),
...
})
```

Clients then receive this grouping of operations, for example invoking the `listUsers` operation in this case might look as follows:

```ts
client.users.list.query();
```
<Snippet name="trpc/organising-operations" />

### Logging

Expand Down
Loading
Loading