nuxt-terraform-skill
Nuxt + Terraform Scaffold Skill
Generate files for Nuxt + AWS infrastructure projects following exact conventions from the terraform-scaffold tool. This skill replaces the CLI — you generate all files directly.
When to Use
- Initializing a new Nuxt + Terraform project structure
- Adding a GraphQL resolver (AppSync JS or Lambda-backed)
- Creating a standalone Lambda function (standard or cron)
- User mentions: resolver, lambda, graphql, appsync, terraform scaffold, dynamodb query
Pre-Requisites
Before scaffolding, check for terraform-scaffold.config.ts in the project root to get:
functionPrefix(PascalCase, e.g.MyApp)environments(default:["staging", "production"])- Custom paths (if any)
If no config exists, ask the user for these values before proceeding.
Directory Structure
All generated projects follow this layout:
<project-root>/
├── terraform-scaffold.config.ts
├── terraform/
│ ├── envs/
│ │ ├── staging/
│ │ │ ├── backend.hcl
│ │ │ ├── main.tf
│ │ │ ├── outputs.tf
│ │ │ ├── variables.tf
│ │ │ ├── versions.tf
│ │ │ ├── terraform.tfvars.example
│ │ │ ├── schema.graphql
│ │ │ ├── lambda_function.tf ← standalone lambdas go here
│ │ │ └── <model>.tf ← per-model resolver TF blocks
│ │ └── production/
│ │ ├── backend.hcl
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ ├── variables.tf
│ │ ├── versions.tf
│ │ └── terraform.tfvars.example
│ ├── functions/
│ │ ├── base.js ← hash validation pipeline function
│ │ ├── invoke.js ← Lambda invoke resolver
│ │ ├── none.js ← NONE datasource resolver
│ │ └── <resolverName>.js ← per-resolver AppSync JS functions
│ ├── lambda/
│ │ ├── tsconfig.json
│ │ ├── src/<PREFIX><Name>/ ← Lambda source directories
│ │ │ ├── index.ts
│ │ │ ├── package.json
│ │ │ ├── AGENTS.md
│ │ │ └── GEMINI.md
│ │ └── dist/ ← built zip files
│ └── modules/ ← 18 Terraform modules (see references/terraform-modules.md)
├── services/ ← AWS service wrappers
├── app/
│ ├── composables/ ← Nuxt composables
│ └── graphql/ ← typed GraphQL constants
└── utils/
└── hash.ts
Naming Conventions (CRITICAL)
| Concept | Convention | Example |
|---|---|---|
| Function prefix | PascalCase | MyApp |
| Full Lambda name | <prefix><PascalSuffix> |
MyAppRedeemNow |
| Resolver name | camelCase | productById |
| GraphQL constant | SCREAMING_SNAKE_CASE | PRODUCT_BY_ID |
| TF module name | appsync_function_<camelName> |
appsync_function_productById |
| TF lambda module | lambda_function_<camelName> |
lambda_function_productById |
| Composable file | use<Model>.ts |
useProduct.ts |
| GraphQL file | <model>.ts (lcfirst) |
product.ts |
| TF file per model | <model>.tf (lcfirst) |
product.tf |
| DynamoDB datasource | appsync_datasource_<modelLower> |
appsync_datasource_product |
| DynamoDB table | dynamodb_<modelLower>s |
dynamodb_products |
| Query index | by<Field> |
byUserId |
String Conversion Rules
- toScreamingSnake:
productById→PRODUCT_BY_ID(split on uppercase boundaries, join with_, uppercase all) - toPascal: capitalize first letter
- lcfirst: lowercase first letter
Command 1: Init
Ask user for:
- Project name (any string)
- Function prefix (PascalCase, e.g.
MyApp) - AWS region (default:
ap-southeast-2) - S3 state bucket name
- DynamoDB lock table name
Then generate files using templates from references/init-templates.md.
Terraform modules — all 18 reusable modules are documented in references/terraform-modules.md. Consult this reference when wiring up resources like appsync_datasource, lambda_function, dynamodb_table, iam_role, etc.
Template placeholders to replace:
{{PROJECT_NAME}}→ raw project name{{PROJECT_KEBAB}}→ kebab-case version{{FUNCTION_PREFIX}}→ PascalCase prefix{{AWS_REGION}}→ chosen region{{STATE_BUCKET}}→ S3 bucket name{{LOCK_TABLE}}→ DynamoDB table name
Static files (no placeholders): Copy exactly from template sources:
terraform/functions/base.js— from templates/init/functions/base.jsterraform/functions/invoke.js— from templates/init/functions/invoke.jsterraform/functions/none.js— from templates/init/functions/none.jsterraform/lambda/tsconfig.json— from templates/init/lambda/tsconfig.jsonservices/cognitoService.ts— from templates/init/services/cognitoService.tsservices/dynamodbService.ts— from templates/init/services/dynamodbService.tsservices/s3Service.ts— from templates/init/services/s3Service.tsservices/sesService.ts— from templates/init/services/sesService.tsservices/netsuiteService.ts— from templates/init/services/netsuiteService.tsservices/textractService.ts— from templates/init/services/textractService.tsapp/composables/useAuthState.ts— from templates/init/composables/useAuthState.tsapp/composables/useCognitoAuth.ts— from templates/init/composables/useCognitoAuth.tsapp/composables/useGraphql.ts— from templates/init/composables/useGraphql.tsutils/hash.ts— from templates/init/utils/hash.ts
Never overwrite existing files — skip if file already exists.
Also add these scripts to package.json:
{
"gen:graphql": "bunx terraform-scaffold graphql",
"gen:lambda": "bunx terraform-scaffold lambda",
"tf:init:staging": "bunx terraform-scaffold tf staging init",
"tf:plan:staging": "bunx terraform-scaffold tf staging plan",
"tf:apply:staging": "bunx terraform-scaffold tf staging apply",
"tf:build:staging": "bunx terraform-scaffold build --env=staging",
"tf:output:staging": "bunx terraform-scaffold tf-output staging",
"tf:init:production": "bunx terraform-scaffold tf production init",
"tf:plan:production": "bunx terraform-scaffold tf production plan",
"tf:apply:production": "bunx terraform-scaffold tf production apply",
"tf:build:production": "bunx terraform-scaffold build --env=production",
"tf:output:production": "bunx terraform-scaffold tf-output production",
"tf:sync-modules": "bunx terraform-scaffold sync-modules"
}
Command 2: GraphQL Resolver
Ask user for:
- Model name — must be a
@modeltype fromschema.graphql(PascalCase, e.g.Product) - Resolver type —
queryormutation - Resolver name — camelCase (e.g.
productById) - Runtime —
APPSYNC_JSorLAMBDA - DynamoDB operation (APPSYNC_JS only) — one of:
GetItem,Query,PutItem,UpdateItem,Scan,BatchDeleteItem - Fields — which model fields to include as arguments + optional extras (
payload: AWSJSON,filter: AWSJSON,limit: Int,nextToken: String)
What to generate:
A) Schema injection
Insert field into type Query or type Mutation block in schema.graphql:
<name>(<field>: <Type>, ...): <Model> # singular for GetItem, PutItem, UpdateItem
<name>(<field>: <Type>, ...): [<Model>] # list for Query, Scan, BatchDeleteItem
List operations (return [Model]): Query, Scan, BatchDeleteItem
Singular operations (return Model): GetItem, PutItem, UpdateItem
Extra field type mappings: payload → AWSJSON, filter → AWSJSON, limit → Int, nextToken → String
B) GraphQL constant — app/graphql/<model>.ts
Create or append:
export const PRODUCT_BY_ID = `
query ProductById($id: ID!) {
productById(id: $id) {
id
name
createdAt
updatedAt
}
}
`
- Operation type:
queryormutation - Operation name: PascalCase of resolver name
- Variable declarations use GraphQL types from schema (
$field: Type) - Response fields: only scalar fields from the model (filter out relation/object types)
C) Terraform modules — terraform/envs/staging/<model>.tf
For APPSYNC_JS runtime — append:
module "appsync_function_<name>" {
source = "../../modules/appsync_function"
api_id = module.appsync.graphql_api_id
function_name = "<name>"
data_source_name = module.appsync_datasource_<modelLower>.data_source_name
code_path = "../../functions/<name>.js"
}
module "appsync_pipeline_resolver_<name>" {
source = "../../modules/appsync_pipeline_resolver"
api_id = module.appsync.graphql_api_id
type = "<Query|Mutation>"
field = "<name>"
code_path = "../../functions/base.js"
function_ids = [module.appsync_function_<name>.function_id]
}
If module "dynamodb_<modelLower>s" is missing from <model>.tf, generate it:
module "dynamodb_<modelLower>s" {
source = "../../modules/dynamodb_table"
name = "${var.PROJECT_ENV}<Model>s"
hash_key = "id"
attributes = [{ name = "id", type = "S" }]
}
Ask the user to confirm the hash_key, range_key, attributes, and GSIs based on their schema model.
If module "appsync_datasource_<modelLower>" is missing, generate it:
module "appsync_datasource_<modelLower>" {
source = "../../modules/appsync_datasource"
api_id = module.appsync.graphql_api_id
table_name = module.dynamodb_<modelLower>s.table_name
service_role_arn = module.role.appsync_role_arn
}
If any of the following modules are missing from main.tf, generate them using the templates below. These form the core dependency chain: cognito_user_pool → cognito_user_pool_client + appsync → role.
Generation templates for missing dependency modules in main.tf:
module "cognito_user_pool" {
source = "../../modules/cognito_user_pool"
project = var.PROJECT_ENV
from_email_address = "<ask user>"
ses_identity_arn = "<ask user>"
pre_signup_lambda_arn = "<ask user or set to empty string>"
custom_message_lambda_arn = "<ask user or set to empty string>"
post_confirmation_lambda_arn = "<ask user or set to empty string>"
}
module "cognito_user_pool_client" {
source = "../../modules/cognito_user_pool_client"
project = var.PROJECT_ENV
user_pool_id = module.cognito_user_pool.user_pool_id
}
module "appsync" {
source = "../../modules/appsync"
project = var.PROJECT_ENV
aws_region = var.AWS_REGION
schema_path = "${path.module}/schema.graphql"
user_pool_id = module.cognito_user_pool.user_pool_id
}
module "role" {
source = "../../modules/iam_role"
name = var.PROJECT_ENV
}
Ask the user to provide values for placeholders marked <ask user>. For Lambda trigger ARNs, if the trigger Lambdas don't exist yet, the user should create them first or provide empty strings temporarily.
For LAMBDA runtime — append (wrapped in # Note: <name> START/END):
# Note: <name> START
module "lambda_function_<name>" {
source = "../../modules/lambda_function"
lambda_function_name = "${var.PROJECT_ENV}<PascalName>"
zip_path = "../../${path.module}/lambda/dist/${var.PROJECT_ENV}<PascalName>.zip"
handler = "index.handler"
lambda_role_arn = module.role.lambda_role_arn
environment_variables = {
ENV = var.ENV
SERVER_VERSION = local.SERVER_VERSION
PROJECT = var.PROJECT_ENV
}
}
module "appsync_datasource_<name>" {
source = "../../modules/appsync_datasource"
api_id = module.appsync.graphql_api_id
lambda_arn = module.lambda_function_<name>.lambda_function_arn
service_role_arn = module.role.appsync_role_arn
}
module "appsync_function_<name>" {
source = "../../modules/appsync_function"
api_id = module.appsync.graphql_api_id
function_name = "<name>"
data_source_name = module.appsync_datasource_<name>.data_source_name
code_path = "../../functions/invoke.js"
}
module "appsync_pipeline_resolver_<name>" {
source = "../../modules/appsync_pipeline_resolver"
api_id = module.appsync.graphql_api_id
type = "<Query|Mutation>"
field = "<name>"
code_path = "../../functions/base.js"
function_ids = [module.appsync_function_<name>.function_id]
}
# Note: <name> END
D) AppSync JS function file (APPSYNC_JS only) — terraform/functions/<name>.js
Use the DynamoDB operation template from templates/graphql/functions/ (see references/graphql-functions.md for the index).
For GetItem and Query templates, replace:
{{FIELD}}→ the key field name (e.g.id,userId){{INDEX}}→by<Field>(e.g.byUserId) — Query only
E) Lambda source (LAMBDA only) — terraform/lambda/src/<PREFIX><PascalName>/
Create 4 files:
index.ts— handler stub with{{FULL_NAME}}replacedpackage.json—{"version":"0.0.1","lastBuildAt":""}AGENTS.md— from template with{{DESCRIPTION}}="TODO: describe <Name> lambda purpose."GEMINI.md— just@./AGENTS.md
See templates/lambda/ (index at references/lambda-templates.md).
F) Composable — app/composables/use<Model>.ts
If file doesn't exist, create new composable:
import type { <Model> } from '~~/types/<Model>'
import { <CONST_NAME> } from '~/graphql/<modelLower>'
import useGraphql from '~/composables/useGraphql'
export function use<Model>() {
const { getAccessToken } = useCognitoAuth()
async function <name>(<params>) {
const token = await getAccessToken()
const { data, error } = await useGraphql<{ <name>: <ResponseType> }>(
<CONST_NAME>,
{ <fields> },
{ key: 'fetch:<name>', token }
)
if (error.value) throw error.value
return data.value?.data?.<name> ?? <fallback>
}
return { <name> }
}
If file exists, inject:
- Add import for the new GraphQL constant
- Add async function before
return { - Add function name to
return { ... }
Response types: <Model> for singular ops, <Model>[] for list ops.
Fallback: null for singular, [] for list.
Param types: Map GraphQL types → TS: ID/String/AWSDateTime/AWSJSON → string, Int/Float → number, Boolean → boolean.
Command 3: Lambda Function
Ask user for:
- Name — PascalCase suffix (e.g.
RedeemNow) - Type —
standardorcron - Schedule (cron only) — EventBridge expression (e.g.
rate(5 minutes))
Full name = <functionPrefix><Name> (e.g. MyAppRedeemNow).
camelSuffix = lcfirst of Name (e.g. redeemNow).
What to generate:
A) Lambda source — terraform/lambda/src/<fullName>/
Same 4 files as GraphQL LAMBDA runtime (see above).
B) TF module — append to terraform/envs/staging/lambda_function.tf:
module "lambda_function_<camelSuffix>" {
source = "../../modules/lambda_function"
lambda_function_name = "${var.PROJECT_ENV}<PascalSuffix>"
zip_path = "../../${path.module}/lambda/dist/${var.PROJECT_ENV}<PascalSuffix>.zip"
handler = "index.handler"
lambda_role_arn = module.role.lambda_role_arn
environment_variables = {
ENV = var.ENV
SERVER_VERSION = local.SERVER_VERSION
PROJECT = var.PROJECT_ENV
}
}
If module "role" is missing from main.tf, generate it using the generation templates in Command 2 Section C above. If module "appsync", module "cognito_user_pool", or module "cognito_user_pool_client" are also missing, generate them too — follow the full dependency chain.
C) Cron resources (cron type only) — append after module block:
# <PascalSuffix> CRON START
resource "aws_cloudwatch_event_rule" "lambda_cron_<camelSuffix>" {
name = "${var.PROJECT_ENV}<PascalSuffix>Schedule"
schedule_expression = "<schedule>"
}
resource "aws_cloudwatch_event_target" "lambda_cron_<camelSuffix>" {
rule = aws_cloudwatch_event_rule.lambda_cron_<camelSuffix>.name
target_id = "${var.PROJECT_ENV}<PascalSuffix>"
arn = module.lambda_function_<camelSuffix>.lambda_function_arn
}
resource "aws_lambda_permission" "lambda_cron_<camelSuffix>" {
statement_id = "AllowExecutionFromEventBridge<PascalSuffix>"
action = "lambda:InvokeFunction"
function_name = module.lambda_function_<camelSuffix>.lambda_function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.lambda_cron_<camelSuffix>.arn
}
# <PascalSuffix> CRON END
Idempotency Rules
- Never overwrite existing files during init
- Skip if a TF module block already exists (check for
module "appsync_function_<name>") - Skip if a schema field already exists (check for
<name>(in schema) - Skip if a GraphQL constant already exists (check for
export const <CONST_NAME>) - Skip if a composable function already exists (check for
async function <name>() - Skip Lambda source dir if it already exists
- When appending to files, trim trailing whitespace and add newline before new content
Checklist Before Generating
- Read
terraform-scaffold.config.tsforfunctionPrefix - Read
schema.graphqlto understand existing models and fields - Check existing TF files for duplicate modules
- Check existing graphql/ files for duplicate constants
- Check existing composables for duplicate functions
- Follow exact naming conventions (camelCase resolver, SCREAMING_SNAKE constant, etc.)
- Use exact indentation and formatting from templates
- Check referenced module dependencies exist in TF files; if missing, generate them:
- APPSYNC_JS resolver: needs
module "dynamodb_<modelLower>s"andmodule "appsync_datasource_<modelLower>"in<model>.tf - LAMBDA resolver: needs
module "role"(inmain.tf) — lambda_role_arn + appsync_role_arn - Standalone Lambda: needs
module "role"(inmain.tf) — lambda_role_arn - All resolvers: needs
module "appsync"(inmain.tf) — graphql_api_id
- APPSYNC_JS resolver: needs
- When generating missing dependency modules, use the module signatures from references/terraform-modules.md and wire them with the correct variables from existing modules
Post-Generation Validation
After generating or modifying any .tf files, always run these steps from the env directory (e.g. terraform/envs/staging/):
- Format —
terraform fmt <file>on each generated/modified.tffile to auto-fix formatting - Validate —
terraform validateto check for syntax errors, missing references, and config issues - Fix — if validation reports errors, read the error output, fix the generated code, and re-run
terraform fmt+terraform validateuntil it passes - Skip gracefully — if
terraform validatefails because providers are not initialized (terraform initnot run), skip validation and inform the user they need to runterraform initfirst
More from ralphcrisostomo/nuxt-development-skills
ralph
Convert PRDs to prd.json format for the Ralph autonomous agent system. Use when you have an existing PRD and need to convert it to Ralph's JSON format. Triggers on: convert this prd, turn this into ralph format, create prd.json from this, ralph json.
52prd
Generate a Product Requirements Document (PRD) for a new feature. Use when planning a feature, starting a new project, or when asked to create a PRD. Triggers on: create a prd, write prd for, plan this feature, requirements for, spec out.
39optimise-claude
Use when auditing, trimming, or restructuring AI instruction files (CLAUDE.md, SKILL.md, AGENTS.md) to reduce context-window consumption. Trigger whenever CLAUDE.md is bloated or Claude ignores instructions, a SKILL.md exceeds 120 lines, skills share duplicated content, AGENTS.md has large inline blocks, or the user asks to optimize, slim down, or reduce token usage.
37nuxt-init
Use when scaffolding a new Nuxt 4 project with standard config files (prettier, eslint, gitignore, husky, vitest, tsconfig, sops) and bun scripts.
33nuxt-terraform
Scaffold Nuxt + AWS Terraform infrastructure. Use when adding GraphQL resolvers, Lambda functions, initializing a new project with AppSync, DynamoDB, Cognito, writing Terraform tests, or generating/reviewing Terraform code style. Triggers on: add graphql resolver, create lambda, scaffold terraform, init terraform, add appsync resolver, add mutation, add query, add terraform test, write tftest, terraform style.
32todo
Use when scanning a codebase for incomplete work and maintaining a living TODO.md grouped by feature. Triggers on: scan for todos, find incomplete work, update todo, what needs doing, create todo list.
30