Set up a monorepo
Task 1: Create a monorepo
Section titled “Task 1: Create a monorepo”To create a new monorepo, from within your desired directory, run the following command:
npx create-nx-workspace@21.6.8 dungeon-adventure --pm=pnpm --preset=@aws/nx-plugin --iacProvider=CDK --ci=skip --aiAgentsnpx create-nx-workspace@21.6.8 dungeon-adventure --pm=yarn --preset=@aws/nx-plugin --iacProvider=CDK --ci=skip --aiAgentsnpx create-nx-workspace@21.6.8 dungeon-adventure --pm=npm --preset=@aws/nx-plugin --iacProvider=CDK --ci=skip --aiAgentsnpx create-nx-workspace@21.6.8 dungeon-adventure --pm=bun --preset=@aws/nx-plugin --iacProvider=CDK --ci=skip --aiAgentsThis will set up a NX monorepo within the dungeon-adventure directory. When you open the directory in VSCode, you will see this file structure:
Directory.nx/
- …
Directory.vscode/
- …
Directorynode_modules/
- …
Directorypackages/ this is where your sub-projects will reside
- …
- .gitignore
- .npmrc
- .prettierignore
- .prettierrc
- nx.json configures the Nx CLI and monorepo defaults
- package.json all node dependencies are defined here
- pnpm-lock.yaml or bun.lock, yarn.lock, package-lock.json depending on package manager
- pnpm-workspace.yaml if using pnpm
- README.md
- tsconfig.base.json all node based sub-projects extend this
- tsconfig.json
- aws-nx-plugin.config.mts configuraton for the Nx Plugin for AWS
We can now start creating our different sub-projects using the @aws/nx-plugin.
Task 2: Create a Game API
Section titled “Task 2: Create a Game API”First, let’s create our Game API. To do this, create a tRPC API called GameApi using these steps:
- 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 - ts#trpc-api - Fill in the required parameters
- name: GameApi
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactiveyarn nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactivenpx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactivebunx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-runYou will see some new files appear in your file tree.
ts#trpc-api updated files
Below is a list of all files which have been generated by the ts#trpc-api generator. We are going to examine some of the key files highlighted in the file tree:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ app specific cdk constructs
Directoryapis/
- game-api.ts cdk construct to create your tRPC API
- index.ts
- …
- index.ts
Directorycore/ generic cdk constructs
Directoryapi/
- rest-api.ts base cdk construct for an API Gateway Rest API
- trpc-utils.ts utilities for trpc API CDK constructs
- utils.ts utilities for API constructs
- index.ts
- runtime-config.ts
- index.ts
- project.json
- …
Directorytypes/ shared types
Directorysrc/
- index.ts
- runtime-config.ts interface definition used by both CDK and website
- project.json
- …
Directorygame-api/ tRPC API
Directorysrc/
Directoryclient/ vanilla client typically used for ts machine to machine calls
- index.ts
- sigv4.ts
Directorymiddleware/ powertools instrumentation
- error.ts
- index.ts
- logger.ts
- metrics.ts
- tracer.ts
Directoryschema/ definitions of inputs and outputs for your API
- echo.ts
Directoryprocedures/ specific implementations for your API procedures/routes
- echo.ts
- index.ts
- init.ts sets up context and middleware
- local-server.ts used when running the tRPC server locally
- router.ts entrypoint for your lambda handler which defines all procedures
- project.json
- …
- eslint.config.mjs
- vitest.workspace.ts
Let us look at these key files:
import { awsLambdaRequestHandler, CreateAWSLambdaContextOptions,} from '@trpc/server/adapters/aws-lambda';import { echo } from './procedures/echo.js';import { t } from './init.js';import { APIGatewayProxyEvent } from 'aws-lambda';
export const router = t.router;
export const appRouter = router({ echo,});
export const handler = awsLambdaRequestHandler({ router: appRouter, createContext: ( ctx: CreateAWSLambdaContextOptions<APIGatewayProxyEvent>, ) => ctx, responseMeta: () => ({ headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': '*', }, }),});
export type AppRouter = typeof appRouter;The router defines the entrypoint for your tRPC API and is the place where you will declare all of your API methods. As you can see above, we have a method called echo with it’s implementation in the ./procedures/echo.ts file.
import { publicProcedure } from '../init.js';import { EchoInputSchema, EchoOutputSchema,} from '../schema/echo.js';
export const echo = publicProcedure .input(EchoInputSchema) .output(EchoOutputSchema) .query((opts) => ({ result: opts.input.message }));This file is the implementation of the echo method and as you can see is strongly typed by declaring its input and output data structures.
import { z } from 'zod';
export const EchoInputSchema = z.object({ message: z.string(),});
export type IEchoInput = z.TypeOf<typeof EchoInputSchema>;
export const EchoOutputSchema = z.object({ result: z.string(),});
export type IEchoOutput = z.TypeOf<typeof EchoOutputSchema>;All tRPC schema definitions are defined using Zod and are exported as typescript types via the z.TypeOf syntax.
import { Construct } from 'constructs';import * as url from 'url';import { Code, Runtime, Function, FunctionProps, Tracing,} from 'aws-cdk-lib/aws-lambda';import { AuthorizationType, Cors, LambdaIntegration,} from 'aws-cdk-lib/aws-apigateway';import { Duration, Stack } from 'aws-cdk-lib';import { PolicyDocument, PolicyStatement, Effect, AccountPrincipal, AnyPrincipal,} from 'aws-cdk-lib/aws-iam';import { IntegrationBuilder, RestApiIntegration,} from '../../core/api/utils.js';import { RestApi } from '../../core/api/rest-api.js';import { Procedures, routerToOperations } from '../../core/api/trpc-utils.js';import { AppRouter, appRouter } from ':dungeon-adventure/game-api';
// String union type for all API operation namestype Operations = Procedures<AppRouter>;
/** * Properties for creating a GameApi construct * * @template TIntegrations - Map of operation names to their integrations */export interface GameApiProps< TIntegrations extends Record<Operations, RestApiIntegration>,> { /** * Map of operation names to their API Gateway integrations */ integrations: TIntegrations;}
/** * A CDK construct that creates and configures an AWS API Gateway REST API * specifically for GameApi. * @template TIntegrations - Map of operation names to their integrations */export class GameApi< TIntegrations extends Record<Operations, RestApiIntegration>,> extends RestApi<Operations, TIntegrations> { /** * Creates default integrations for all operations, which implement each operation as * its own individual lambda function. * * @param scope - The CDK construct scope * @returns An IntegrationBuilder with default lambda integrations */ public static defaultIntegrations = (scope: Construct) => { return IntegrationBuilder.rest({ operations: routerToOperations(appRouter), defaultIntegrationOptions: { runtime: Runtime.NODEJS_LATEST, handler: 'index.handler', code: Code.fromAsset( url.fileURLToPath( new URL( '../../../../../../dist/packages/game-api/bundle', import.meta.url, ), ), ), timeout: Duration.seconds(30), tracing: Tracing.ACTIVE, environment: { AWS_CONNECTION_REUSE_ENABLED: '1', }, } satisfies FunctionProps, buildDefaultIntegration: (op, props: FunctionProps) => { const handler = new Function(scope, `GameApi${op}Handler`, props); return { handler, integration: new LambdaIntegration(handler) }; }, }); };
constructor( scope: Construct, id: string, props: GameApiProps<TIntegrations>, ) { super(scope, id, { apiName: 'GameApi', defaultMethodOptions: { authorizationType: AuthorizationType.IAM, }, defaultCorsPreflightOptions: { allowOrigins: Cors.ALL_ORIGINS, allowMethods: Cors.ALL_METHODS, }, policy: new PolicyDocument({ statements: [ // Here we grant any AWS credentials from the account that the project is deployed in to call the api. // Machine to machine fine-grained access can be defined here using more specific principals (eg roles or // users) and resources (eg which api paths may be invoked by which principal) if required. new PolicyStatement({ effect: Effect.ALLOW, principals: [new AccountPrincipal(Stack.of(scope).account)], actions: ['execute-api:Invoke'], resources: ['execute-api:/*'], }), // Open up OPTIONS to allow browsers to make unauthenticated preflight requests new PolicyStatement({ effect: Effect.ALLOW, principals: [new AnyPrincipal()], actions: ['execute-api:Invoke'], resources: ['execute-api:/*/OPTIONS/*'], }), ], }), operations: routerToOperations(appRouter), ...props, }); }}This is the CDK construct that defines our GameApi. It provides a defaultIntegrations method which automatically creates a Lambda function for each procedure in our tRPC API, pointing to the bundled API implementation. This means that at cdk synth time, bundling does not occur (opposed to using NodeJsFunction) as we have already bundled it as part of the backend project’s build target.
Task 3: Create Story agents
Section titled “Task 3: Create Story agents”Now let’s create our Story Agents.
Story agent: Python project
Section titled “Story agent: Python project”To create a Python project:
- 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 - py#project - Fill in the required parameters
- name: story
- Click
Generate
pnpm nx g @aws/nx-plugin:py#project --name=story --no-interactiveyarn nx g @aws/nx-plugin:py#project --name=story --no-interactivenpx nx g @aws/nx-plugin:py#project --name=story --no-interactivebunx nx g @aws/nx-plugin:py#project --name=story --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-runyarn nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-runnpx nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-runbunx nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-runYou will see some new files appear in your file tree.
py#project updated files
The py#project generates these files:
Directory.venv/ single virtual env for monorepo
- …
Directorypackages/
Directorystory/
Directorydungeon_adventure_story/ python module
- hello.py example python file (we’ll ignore this)
Directorytests/
- …
- .python-version
- pyproject.toml
- project.json
- .python-version pinned uv python version
- pyproject.toml
- uv.lock
This has configured a Python project and UV Workspace with shared virtual environment.
Story agent: Strands Agent
Section titled “Story agent: Strands Agent”To add a Strands agent to the project with the py#strands-agent 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 - py#strands-agent - Fill in the required parameters
- project: story
- Click
Generate
pnpm nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactiveyarn nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactivenpx nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactivebunx nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive --dry-runyarn nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive --dry-runnpx nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive --dry-runbunx nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive --dry-runYou will see some new files appear in your file tree.
py#strands-agent updated files
The py#strands-agent generates these files:
Directorypackages/
Directorystory/
Directorydungeon_adventure_story/ python module
Directoryagent/
- main.py entrypoint for your agent in Bedrock AgentCore Runtime
- agent.py defines an example agent and tools
- agentcore_mcp_client.py utility for creating clients to interact with MCP servers
- Dockerfile defines the docker image for deployment to AgentCore Runtime
Directorycommon/constructs/
Directorysrc
Directorycore/agent-core/
- runtime.ts generic construct for deploying to AgentCore Runtime
Directoryapp/agents/story-agent/
- story-agent.ts construct for deploying your Story agent to AgentCore Runtime
Let’s take a look at some of the files in detail:
from contextlib import contextmanager
from strands import Agent, toolfrom strands_tools import current_time
# Define a custom tool@tooldef add(a: int, b: int) -> int: return a + b
@contextmanagerdef get_agent(session_id: str): yield Agent( system_prompt="""You are an addition wizard.Use the 'add' tool for addition tasks.Refer to tools as your 'spellbook'.""", tools=[add, current_time], )This creates an example Strands agent and defines an addition tool.
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from .agent import get_agent
app = BedrockAgentCoreApp()
@app.entrypointasync def invoke(payload, context): """Handler for agent invocation""" prompt = payload.get( "prompt", "No prompt found in input, please guide the user " "to create a json payload with prompt key" )
with get_agent(session_id=context.session_id) as agent: stream = agent.stream_async(prompt) async for event in stream: print(event) yield (event)
if __name__ == "__main__": app.run()This is the entrypoint for the agent, configured using the Amazon Bedrock AgentCore SDK. It uses Strands support for streaming and streams events back to the client as they occur.
import { Lazy, Names } from 'aws-cdk-lib';import { DockerImageAsset, Platform } from 'aws-cdk-lib/aws-ecr-assets';import { Construct } from 'constructs';import { execSync } from 'child_process';import * as path from 'path';import * as url from 'url';import { AgentCoreRuntime, AgentCoreRuntimeProps,} from '../../../core/agent-core/runtime.js';
export type StoryAgentProps = Omit< AgentCoreRuntimeProps, 'runtimeName' | 'serverProtocol' | 'containerUri'>;
export class StoryAgent extends Construct { public readonly dockerImage: DockerImageAsset; public readonly agentCoreRuntime: AgentCoreRuntime;
constructor(scope: Construct, id: string, props?: StoryAgentProps) { super(scope, id);
this.dockerImage = new DockerImageAsset(this, 'DockerImage', { platform: Platform.LINUX_ARM64, directory: path.dirname(url.fileURLToPath(new URL(import.meta.url))), extraHash: execSync( `docker inspect dungeon-adventure-story-agent:latest --format '{{.Id}}'`, { encoding: 'utf-8' }, ).trim(), });
this.agentCoreRuntime = new AgentCoreRuntime(this, 'StoryAgent', { runtimeName: Lazy.string({ produce: () => Names.uniqueResourceName(this.agentCoreRuntime, { maxLength: 40 }), }), serverProtocol: 'HTTP', containerUri: this.dockerImage.imageUri, ...props, }); }}This configures a CDK DockerImageAsset which uploads your agent Docker image to ECR, and hosts it using AgentCore Runtime.
You may notice an extra Dockerfile, that references the Docker image from the story project, allowing us to co-locate the Dockerfile and agent source code.
Task 4: Set up inventory tools
Section titled “Task 4: Set up inventory tools”Inventory: TypeScript project
Section titled “Inventory: TypeScript project”Let us create an MCP server to provide tools for our Story Agent to manage a player’s inventory.
First, we create a TypeScript project:
- 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 - ts#project - Fill in the required parameters
- name: inventory
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#project --name=inventory --no-interactiveyarn nx g @aws/nx-plugin:ts#project --name=inventory --no-interactivenpx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactivebunx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-runThis will create an empty TypeScript project.
ts#project updated files
The ts#project generator generates these files.
Directorypackages/
Directoryinventory/
Directorysrc/
- index.ts entry point with example function
- project.json project configuration
- eslint.config.mjs lint configuration
- vite.config.ts test configuration
- tsconfig.json base typescript configuration for the project
- tsconfig.lib.json typescript configuration for the project targeted for compilation and bundling
- tsconfig.spec.json typescript configuration for tests
- tsconfig.base.json updated to configure an alias for other projects to reference this
Inventory: MCP server
Section titled “Inventory: MCP server”Next, we’ll add an MCP server to our TypeScript project:
- 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 - ts#mcp-server - Fill in the required parameters
- project: inventory
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactiveyarn nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactivenpx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactivebunx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-runThis will add an MCP server.
ts#mcp-server updated files
The ts#mcp-server generator generates these files.
Directorypackages/
Directoryinventory/
Directorysrc/mcp-server/
- server.ts creates the MCP server
Directorytools/
- add.ts example tool
Directoryresources/
- sample-guidance.ts example resource
- stdio.ts entry point for MCP with STDIO transport
- http.ts entry point for MCP with Streamable HTTP transport
- Dockerfile builds the image for AgentCore Runtime
- rolldown.config.ts configuration for bundling the MCP server for deployment to AgentCore
Directorycommon/constructs/
Directorysrc
Directoryapp/mcp-servers/inventory-mcp-server/
- inventory-mcp-server.ts construct for deploying your inventory MCP server to AgentCore Runtime
Task 5: Create the User Interface (UI)
Section titled “Task 5: Create the User Interface (UI)”In this task, we will create the UI which will allow you to interact with the game.
Game UI: Website
Section titled “Game UI: Website”To create the UI, create a website called GameUI using these steps:
- 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 - ts#react-website - Fill in the required parameters
- name: GameUI
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactiveyarn nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactivenpx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactivebunx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-runYou will see some new files appear in your file tree.
ts#react-website updated files
The ts#react-website generates these files. Let us examine some of the key files highlighted in the file tree:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ app specific cdk constructs
Directorystatic-websites/
- game-ui.ts cdk construct to create your Game UI
Directorycore/
- static-website.ts generic static website construct
Directorygame-ui/
Directorypublic/
- …
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.ts overall page layout: header, footer, sidebar, etc
- navitems.ts sidebar nav items
Directoryhooks/
- useAppLayout.tsx allows you to dynamically set things like notifications, page style, etc
Directoryroutes/ @tanstack/react-router file based routes
- index.tsx root ’/’ page redirects to ‘/welcome’
- __root.tsx all pages use this component as a base
Directorywelcome/
- index.tsx
- config.ts
- main.tsx React entrypoint
- routeTree.gen.ts this is automatically updated by @tanstack/react-router
- styles.css
- index.html
- project.json
- vite.config.ts
- …
import * as url from 'url';import { Construct } from 'constructs';import { StaticWebsite } from '../../core/index.js';
export class GameUI extends StaticWebsite { constructor(scope: Construct, id: string) { super(scope, id, { websiteName: 'GameUI', websiteFilePath: url.fileURLToPath( new URL( '../../../../../../dist/packages/game-ui/bundle', import.meta.url, ), ), }); }}This is the CDK construct that defines our GameUI. It has already configured the file path to the generated bundle for our Vite based UI. This means that at build time, bundling occurs within the game-ui project’s build target and the output is used here.
import React from 'react';import { createRoot } from 'react-dom/client';import { I18nProvider } from '@cloudscape-design/components/i18n';import messages from '@cloudscape-design/components/i18n/messages/all.en';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';
import '@cloudscape-design/global-styles/index.css';
const router = createRouter({ routeTree });
// Register the router instance for type safetydeclare module '@tanstack/react-router' { interface Register { router: typeof router; }}
const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <I18nProvider locale="en" messages={[messages]}> <RouterProvider router={router} /> </I18nProvider> </React.StrictMode>, );This is the entry point where React is mounted. As shown, it initially just configures a @tanstack/react-router in a file-based-routing configuration. As long as your development server is running, you can create files within the routes folder and @tanstack/react-router will create the boilerplate file setup for you, along with updating the routeTree.gen.ts file. This file maintains all routes in a type-safe manner, which means when you use <Link>, the to option will only show valid routes.
For more information, refer to the @tanstack/react-router docs.
import { ContentLayout, Header, SpaceBetween, Container,} from '@cloudscape-design/components';import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/welcome/')({ component: RouteComponent,});
function RouteComponent() { return ( <ContentLayout header={<Header>Welcome</Header>}> <SpaceBetween size="l"> <Container>Welcome to your new Cloudscape website!</Container> </SpaceBetween> </ContentLayout> );}A component will be rendered when navigating to the /welcome route. @tanstack/react-router will manage the Route for you whenever you create/move this file (as long as the dev server is running).
Game UI: Auth
Section titled “Game UI: Auth”Let us configure our Game UI to require authenticated access via Amazon Cognito using these steps:
- 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 - ts#react-website#auth - Fill in the required parameters
- cognitoDomain: game-ui
- project: @dungeon-adventure/game-ui
- allowSignup: true
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactiveyarn nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactivenpx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactivebunx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-runYou will see some new files appear/change in your file tree.
ts#react-website#auth updated files
The ts#react-website#auth generator updates/generates these files. Let us examine some of the key files highlighted in the file tree:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
- user-identity.ts cdk construct for creating user/identity pools
Directorytypes/
Directorysrc/
- runtime-config.ts updated to add the cognitoProps
Directorygame-ui/
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.tsx adds the logged in user/logout to the header
DirectoryCognitoAuth/
- index.ts manages logging into Cognito
DirectoryRuntimeConfig/
- index.tsx fetches the
runtime-config.jsonand provides it to children via context
- index.tsx fetches the
Directoryhooks/
- useRuntimeConfig.tsx
- main.tsx Updated to add Cognito
import CognitoAuth from './components/CognitoAuth';import RuntimeConfigProvider from './components/RuntimeConfig';import React from 'react';import { createRoot } from 'react-dom/client';import { I18nProvider } from '@cloudscape-design/components/i18n';import messages from '@cloudscape-design/components/i18n/messages/all.en';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';import '@cloudscape-design/global-styles/index.css';const router = createRouter({ routeTree });// Register the router instance for type safetydeclare module '@tanstack/react-router' { interface Register { router: typeof router; }}const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <I18nProvider locale="en" messages={[messages]}> <RuntimeConfigProvider> <CognitoAuth> <RouterProvider router={router} /> </CognitoAuth> </RuntimeConfigProvider> </I18nProvider> </React.StrictMode>, );The RuntimeConfigProvider and CognitoAuth components have been added to the main.tsx file via an AST transform. This allows the CognitoAuth component to authenticate with Amazon Cognito by fetching the runtime-config.json which contains the required cognito connection configuration in order to make the backend calls to the correct destination.
Game UI: Connect to Game API
Section titled “Game UI: Connect to Game API”Let us configure our Game UI to connect to our previously created Game API.
- 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 - api-connection - Fill in the required parameters
- sourceProject: @dungeon-adventure/game-ui
- targetProject: @dungeon-adventure/game-api
- Click
Generate
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactiveyarn nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactivenpx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactivebunx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-runyarn nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-runnpx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-runbunx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-runYou will see some new files have appear/change in your file tree.
UI -> tRPC api-connection updated files
The api-connection generator generates/updates these files. Let us examine some of the key files highlighted in the file tree:
Directorypackages/
Directorygame-ui/
Directorysrc/
Directorycomponents/
- GameApiClientProvider.tsx sets up the GameAPI client
Directoryhooks/
- useGameApi.tsx hooks to call the GameApi
- main.tsx injects the trpc client providers
- package.json
import { GameApiTRCPContext } from '../components/GameApiClientProvider';
export const useGameApi = GameApiTRCPContext.useTRPC;This hook uses tRPC’s latest React Query integration allowing users to interact with @tanstack/react-query directly without any additional layers of abstraction. For examples on how to call tRPC APIs, refer to the using the tRPC hook guide.
import GameApiClientProvider from './components/GameApiClientProvider';import QueryClientProvider from './components/QueryClientProvider';import CognitoAuth from './components/CognitoAuth';import RuntimeConfigProvider from './components/RuntimeConfig';import React from 'react';import { createRoot } from 'react-dom/client';import { I18nProvider } from '@cloudscape-design/components/i18n';import messages from '@cloudscape-design/components/i18n/messages/all.en';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';import '@cloudscape-design/global-styles/index.css';const router = createRouter({ routeTree });// Register the router instance for type safetydeclare module '@tanstack/react-router' { interface Register { router: typeof router; }}const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <I18nProvider locale="en" messages={[messages]}> <RuntimeConfigProvider> <CognitoAuth> <QueryClientProvider> <GameApiClientProvider> <RouterProvider router={router} /> </GameApiClientProvider> </QueryClientProvider> </CognitoAuth> </RuntimeConfigProvider> </I18nProvider> </React.StrictMode>, );The main.tsx file has been updated via an AST transform to inject the tRPC providers.
Game UI: Infrastructure
Section titled “Game UI: Infrastructure”Let us create the final sub-project for the CDK infrastructure.
- 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 - ts#infra - Fill in the required parameters
- name: infra
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactiveyarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactivenpx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactivebunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runYou will see some new files have appear/change in your file tree.
ts#infra updated files
The ts#infra generator generates/updates these. Let us examine some of the key files highlighted in the file tree:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
- checkov.ts
- index.ts
Directoryinfra
Directorysrc/
Directorystages/
- application-stage.ts cdk stacks defined here
Directorystacks/
- application-stack.ts cdk resources defined here
- index.ts
- main.ts entrypoint which defines all stages
- cdk.json
- project.json
- …
- package.json
- tsconfig.json add references
- tsconfig.base.json add alias
import { ApplicationStage } from './stacks/application-stage.js';import { App } from ':dungeon-adventure/common-constructs';
const app = new App();
// Use this to deploy your own sandbox environment (assumes your CLI credentials)new ApplicationStage(app, 'dungeon-adventure-infra-sandbox', { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, },});
app.synth();This is the entry point for your CDK application.
import * as cdk from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props);
// The code that defines your stack goes here }}Let us instantiate our CDK constructs to build our dungeon adventure game.
Task 6: Update our infrastructure
Section titled “Task 6: Update our infrastructure”Let’s update packages/infra/src/stacks/application-stack.ts to instantiate some of our generated constructs:
import { GameApi, GameUI, InventoryMcpServer, RuntimeConfig, StoryAgent, UserIdentity,} from ':dungeon-adventure/common-constructs';import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
const userIdentity = new UserIdentity(this, 'UserIdentity');
const gameApi = new GameApi(this, 'GameApi', { integrations: GameApi.defaultIntegrations(this).build(), });
const { userPool, userPoolClient } = userIdentity;
const mcpServer = new InventoryMcpServer(this, 'InventoryMcpServer');
// Use Cognito for user authentication with the agent const storyAgent = new StoryAgent(this, 'StoryAgent', { authorizerConfiguration: { customJwtAuthorizer: { discoveryUrl: `https://cognito-idp.${Stack.of(userPool).region}.amazonaws.com/${userPool.userPoolId}/.well-known/openid-configuration`, allowedAudience: [userPoolClient.userPoolClientId], }, }, environment: { INVENTORY_MCP_ARN: mcpServer.agentCoreRuntime.arn, }, }); // Add the Story Agent ARN to runtime-config.json so it can be referenced by the website RuntimeConfig.ensure(this).config.agentArn = storyAgent.agentCoreRuntime.arn;
new CfnOutput(this, 'StoryAgentArn', { value: storyAgent.agentCoreRuntime.arn, }); new CfnOutput(this, 'InventoryMcpArn', { value: mcpServer.agentCoreRuntime.arn, });
// Grant the agent permissions to invoke our mcp server mcpServer.agentCoreRuntime.grantInvoke(storyAgent.agentCoreRuntime);
// Grant the authenticated role access to invoke the api gameApi.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
// Ensure this is instantiated last so our runtime-config.json can be automatically configured new GameUI(this, 'GameUI'); }}import { Stack, StackProps } from 'aws-cdk-lib';import { GameApi, GameUI, InventoryMcpServer, RuntimeConfig, StoryAgent, UserIdentity,} from ':dungeon-adventure/common-constructs';import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
// The code that defines your stack goes here const userIdentity = new UserIdentity(this, 'UserIdentity');
const gameApi = new GameApi(this, 'GameApi', { integrations: GameApi.defaultIntegrations(this).build(), });
const { userPool, userPoolClient } = userIdentity;
const mcpServer = new InventoryMcpServer(this, 'InventoryMcpServer');
// Use Cognito for user authentication with the agent const storyAgent = new StoryAgent(this, 'StoryAgent', { authorizerConfiguration: { customJwtAuthorizer: { discoveryUrl: `https://cognito-idp.${Stack.of(userPool).region}.amazonaws.com/${userPool.userPoolId}/.well-known/openid-configuration`, allowedAudience: [userPoolClient.userPoolClientId], }, }, environment: { INVENTORY_MCP_ARN: mcpServer.agentCoreRuntime.arn, }, }); // Add the Story Agent ARN to runtime-config.json so it can be referenced by the website RuntimeConfig.ensure(this).config.agentArn = storyAgent.agentCoreRuntime.arn;
new CfnOutput(this, 'StoryAgentArn', { value: storyAgent.agentCoreRuntime.arn, }); new CfnOutput(this, 'InventoryMcpArn', { value: mcpServer.agentCoreRuntime.arn, });
// Grant the agent permissions to invoke our mcp server mcpServer.agentCoreRuntime.grantInvoke(storyAgent.agentCoreRuntime);
// Grant the authenticated role access to invoke the api gameApi.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
// Ensure this is instantiated last so our runtime-config.json can be automatically configured new GameUI(this, 'GameUI'); }}Task 7: Build the code
Section titled “Task 7: Build the code”Nx commands
Single vs Multiple targets
Section titled “Single vs Multiple targets”The run-many command will run a target on multiple listed subprojects (--all will target them all). This ensures dependencies are executed in the correct order.
You can also trigger a build (or any other task) for a single project target by running the target on the project directly. For example, to build the @dungeon-adventure/infra project, run the following command:
pnpm nx run @dungeon-adventure/infra:buildyarn nx run @dungeon-adventure/infra:buildnpx nx run @dungeon-adventure/infra:buildbunx nx run @dungeon-adventure/infra:buildYou can also omit the scope, and use the Nx shorthand syntax if you prefer:
pnpm nx build infrayarn nx build infranpx nx build infrabunx nx build infraVisualizing your dependencies
Section titled “Visualizing your dependencies”To visualize your dependencies, run:
pnpm nx graphyarn nx graphnpx nx graphbunx nx graph
Caching
Section titled “Caching”Nx relies on caching so that you can re-use artifacts from previous builds in order to speed up development. There is some configuration required to get this to work correctly and there may be cases where you want to perform a build without using the cache. To do that, simply append the --skip-nx-cache argument to your command. For example:
pnpm nx run @dungeon-adventure/infra:build --skip-nx-cacheyarn nx run @dungeon-adventure/infra:build --skip-nx-cachenpx nx run @dungeon-adventure/infra:build --skip-nx-cachebunx nx run @dungeon-adventure/infra:build --skip-nx-cacheIf for whatever reason you ever wanted to clear your cache (stored in the .nx folder), you can run the following command:
pnpm nx resetyarn nx resetnpx nx resetbunx nx resetUsing the command line, run:
pnpm nx run-many --target build --allyarn nx run-many --target build --allnpx nx run-many --target build --allbunx nx run-many --target build --allYou will be prompted with the following:
NX The workspace is out of sync
[@nx/js:typescript-sync]: Some TypeScript configuration files are missing project references to the projects they depend on or contain outdated project references.
This will result in an error in CI.
? Would you like to sync the identified changes to get your workspace up to date? …Yes, sync the changes and run the tasksNo, run the tasks without syncing the changesThis message indicates that NX has detected some files which can be updated automatically for you. In this case, it is referring to the tsconfig.json files which do not have Typescript references set up on references projects.
Select the Yes, sync the changes and run the tasks option to proceed. You should notice all of you IDE related import errors get automatically resolved as the sync generator will add the missing typescript references automatically!
All built artifacts are now available within the dist/ folder located at the root of the monorepo. This is a standard pactice when using projects generated by the @aws/nx-plugin as it does not pollute your file-tree with generated files. In the event you want to clean your files, delete the dist/ folder without worrying about generated files being littered throughout the file tree.
Congratulations! You’ve created all of the required sub-projects required to start implementing the core of our AI Dungeon Adventure game. 🎉🎉🎉