You SHOULD import environment variables from $amplify/env/<function-name>
— this provides type-safe access to values defined in defineFunction.
Values are also available at runtime via process.env.VAR_NAME, but the
$amplify/env import is preferred because it gives you compile-time type
checking and autocompletion.
Grant a function access to other Amplify resources:
typescript
const backend = defineBackend({ auth, data, storage, myFunc });// Grant function access to auth, data, and storagebackend.myFunc.resources.lambda.addEnvironment( 'USER_POOL_ID', backend.auth.resources.userPool.userPoolId);backend.data.resources.tables['Todo'].grantReadData(backend.myFunc.resources.lambda);backend.storage.resources.bucket.grantReadWrite(backend.myFunc.resources.lambda);
For data schema access, use allow.resource() in authorization rules:
How .handler() works:.handler() grants AppSync the permission to invoke the Lambda (AppSync→Lambda). The Lambda IS the resolver — it receives the GraphQL event directly. If the Lambda also needs to call the Data API or access DynamoDB tables for side effects, add allow.resource(fn) to the model with resourceGroupName: 'data' on the function to avoid circular dependencies.
typescript
// ❌ CIRCULAR DEPENDENCY — manual table grant in backend.tsbackend.data.resources.tables["Model"].grantReadData(backend.myFn.resources.lambda);// ✅ Use resourceGroupName to co-locate the function in the data stackconst myFn = defineFunction({ name: 'my-fn', resourceGroupName: 'data' });// Then in the schema: allow.resource(myFn) on the model
Gap: The Lambda resolver receives the GraphQL event but does NOT automatically get TABLE_NAME as an environment variable. Your Lambda must either:
Use the Amplify data client (generateClient()) which discovers tables automatically
Explicitly set env vars: myFunction.addEnvironment('TABLE_NAME', backend.data.resources.tables['Todo'].tableName)
When to use which:
a.query() / a.mutation() with .handler() — AppSync-native, type-safe, uses the data schema. Preferred for most custom logic.
API Gateway + Lambda — Use when you need REST endpoints, webhooks, or third-party integrations that require a specific URL.
runtime must be an integer: Use runtime: 22, NOT
runtime: "nodejs22.x". String format causes build errors.
Wrong handler type: REST API (v1) requires APIGatewayProxyHandler
with event.httpMethod; HTTP API (v2) requires APIGatewayProxyHandlerV2
with event.requestContext.http.method. Mixing them causes malformed
responses. Both return { statusCode, body }.
Missing resource access: A function without explicit grants cannot
access auth, data, or storage resources — add grants in backend.ts.
Secrets in plain environment: Sensitive values must use
secret(), not string literals.
createStack name collision: Stack names passed to
backend.createStack() must be unique across the backend.
Duplicate names cause deployment failures.
Missing @types/node: Lambda functions require @types/node in devDependencies. Without it, process.env and Node.js globals cause TypeScript errors. Install: npm install --save-dev @types/node
@types/aws-lambda: Lambda handlers (S3Handler, PreSignUpTriggerHandler, etc.) need this package for TypeScript types. Install at project root or in the function’s directory if it has its own package.json.
AppSync identity typing:event.identity in custom handlers has varying types depending on auth mode. Use type assertion:
dataSource: 'NONE': Using a.handler.custom({ dataSource: 'NONE' }) causes “Data source not found” during deployment. Use a Lambda handler instead, or create the NONE data source explicitly via CDK.
Lambda error types lost: Custom error classes thrown in Lambda arrive at the frontend as generic Error with only the message preserved. Error name, stack, and custom properties are stripped by AppSync. Return structured error data in the response instead.