Local-first AWS learning dashboard for discovering tagged resources and browsing S3 object previews.
The backend uses local AWS credentials for the MVP. The browser should only talk to the NestJS API, never directly to AWS.
┌─────────────┐
│ React Web │
└──────┬──────┘
│ GraphQL
▼
┌─────────────┐
│ NestJS API │
└──────┬──────┘
│ AWS SDK
▼
┌──────────────────────────┐
│ AWS Resources │
│ - Resource Groups │
│ - S3 │
│ - IAM │
│ - ECR │
│ - SSM │
└──────────────────────────┘npm install
cp .env.example .envDefault local settings:
AWS_REGION=eu-north-1
AWS_PROFILE=default
For screenshots or screen sharing, enable frontend-only account id masking:
VITE_MY_AWS_MASK_ACCOUNT_IDS=true
Rendered 12-digit AWS account ids are shown as *****, while API requests still use the real values.
apps/api NestJS backend
apps/web React frontend
packages/shared Shared TypeScript types
specs Project specs
Run from the repo root.
npm run dev:api
npm run dev:web
npm run build
npm run check:esm
npm run test --workspace @my-aws/api
npm run test --workspace @my-aws/web
npm run test:e2e --workspace @my-aws/api
npm run typecheck
npm run lint
npm run format
npm run format:checknpm run dev also exists, but running API and web in separate terminals is easier while the app is small.
The default API test command runs port-free Vitest tests. The e2e command uses Supertest.
API: http://localhost:3000
API health: http://localhost:3000/health
Web: http://localhost:5173
The API exposes GraphQL at /graphql, but the legacy GraphQL Playground UI is disabled explicitly. Use the React app, curl, or a GraphQL client so queries stay visible and intentional.
Example:
curl http://localhost:3000/graphql \
-H 'content-type: application/json' \
-d '{"query":"{ accountInfo { accountId alias region } }"}'This is where GraphQL is plugged into Nest:
GraphQLModule.forRoot(graphqlConfig);It answers: “How does this app enable /graphql?”
This is the Apollo/Nest GraphQL configuration:
driver: ApolloDriver,
autoSchemaFile: true,
playground: false,It answers: “Which GraphQL server adapter are we using, and how is the schema created?”
This is the simplest real GraphQL query:
@Query(() => AccountInfoType)
accountInfo()It answers: “How does a GraphQL query call backend code?”
This defines the GraphQL schema types using decorators:
@ObjectType('AccountInfo')
@Field()It answers: “What shape does GraphQL expose to clients?”
The tag browser uses a backend in-memory cache. There is no database: the cache lives only inside the running NestJS API process, resets when the API restarts, and expires after a configurable TTL. The default TTL is 12 hours.
Configure it in .env:
MY_AWS_TAG_CACHE_TTL_SECONDS=43200
The UI can also bypass the cache with the Refresh buttons.
apps/web/src/routes/tags.tsx renders the tags page.
On mount, useAsyncRouteData(...) calls loadTagGroups(...).
That sends this GraphQL query first:
tagKeys {
key
valueCount
}Then, for each returned key, it sends:
tagValues(key: "Project") {
key
value
resourceCount
}apps/api/src/tags/tags.resolver.ts receives those GraphQL queries.
The resolver is thin: it does not know about AWS or caching. It just calls the service:
tagKeys(refresh) -> tagsService.tagKeys(refresh)
tagValues(key, refresh) -> tagsService.tagValues(key, refresh)
resourcesByTag(key, value, refresh) -> tagsService.resourcesByTag(key, value, refresh)That is how apps/api/src/tags/tags.service.ts gets called.
Inside tags.service.ts, each public method calls the cache gate:
this.tagsSnapshot(refresh);- if
refreshis false and the cached snapshot has not expired, return cached data - if another request is already rebuilding the snapshot, wait for that same in-flight AWS scan
- otherwise call
buildTagsSnapshot(now)to scan AWS and store the result with a new expiry time
After the snapshot exists:
tagKeys(...)derives the list of tag keys from cached resourcestagValues(...)derives values for one key from cached resourcesresourcesByTag(...)filters cached resources by key/value
So /tags may make several GraphQL requests, but the backend should only do one AWS scan per TTL window unless Refresh is clicked.
The Refresh buttons in apps/web/src/routes/tags.tsx and apps/web/src/routes/tagged-resources.tsx tell the backend to rebuild the tag snapshot instead of reusing the cached one.
That gives the user a manual way to pick up AWS tag changes before the TTL expires.
- no stale cache on error
- IAM tag requests are intentionally sequential to avoid throttling; limited concurrency could be added later
- no external cache for multiple API instances