How to Use AWS Amplify to Deploy a Full-Stack Web App (React + Node.js)
You've got a React frontend talking to a Node.js backend, and you need a deployment pipeline that doesn't require you to hand-stitch CloudFront distributions, API Gateway configs, and CodePipeline stages together manually. AWS Amplify Hosting handles the frontend CI/CD side cleanly, but the backend story — what Amplify actually manages versus what you still own — is where most engineers hit unexpected friction on day one.
TL;DR: AWS Amplify Full-Stack Deployment at a Glance
| Concern | Amplify Handles | You Still Own |
|---|---|---|
| Frontend CI/CD | Git-triggered builds, branch previews, CDN distribution | Build spec tuning, environment variable management |
| Backend APIs | Amplify-generated GraphQL/REST via AppSync or API Gateway (Amplify CLI) | Custom Node.js Express servers, external containers |
| Auth | Cognito User Pools via Amplify Auth category | Custom auth flows, external IdPs beyond Cognito |
| Hosting | CloudFront + S3 (managed), custom domains, SSL | Advanced CloudFront behaviors, WAF integration |
| Studio | Visual data modeling, component generation, user management | Complex business logic, non-Amplify backend resources |
How AWS Amplify Works Under the Hood
Amplify is not a single service — it's a layered platform. Understanding the layers prevents the most common misconfigurations.
Amplify Hosting is a managed CI/CD and CDN service. When you connect a Git repository, Amplify provisions a build environment, runs your build commands, and deploys the output to a CloudFront distribution backed by S3. Each branch can map to its own environment with isolated URLs — this is the branch preview feature. The build pipeline is defined in amplify.yml at the repo root.
Amplify CLI + Backend Categories is a separate concern. The CLI provisions AWS resources (AppSync for GraphQL, API Gateway + Lambda for REST, Cognito, DynamoDB, S3) using CloudFormation under the hood. These resources live in your AWS account. The CLI generates a aws-exports.js file that the Amplify JS library uses at runtime to locate endpoints.
Amplify Studio is a visual interface layered on top of the CLI backend. It lets you model data schemas, generate React UI components from Figma designs, and manage Cognito users — but it operates on Amplify-provisioned resources, not arbitrary external backends.
The critical architectural boundary: Amplify Hosting deploys your frontend. Amplify CLI/Studio provisions backend infrastructure. A custom Node.js Express server running on EC2 or ECS is outside Amplify's management scope — Amplify can call it, but it won't manage its lifecycle.
amplify push"] C --> D["CloudFormation Stacks
AppSync / Lambda / Cognito"] C --> E["aws-exports.js Generated"] E --> F["Frontend Phase
npm run build"] F --> G["React Bundle"] G --> H["S3 Artifact Store"] H --> I["CloudFront Distribution"] I --> J["Browser"] J --> D
- Git Push triggers Amplify Hosting's build pipeline via a webhook.
- Build Environment runs your
amplify.ymlcommands, including anyamplify pushfor backend changes. - Backend CloudFormation provisions or updates AppSync, Lambda, Cognito, and DynamoDB stacks in your account.
- Frontend artifacts (React build output) are deployed to S3 and invalidated on CloudFront.
- Runtime: The browser loads the React app from CloudFront, which calls AppSync or API Gateway endpoints provisioned by the CLI.
Setting Up AWS Amplify Hosting for CI/CD
Connect your repository first. Amplify supports GitHub, GitLab, Bitbucket, and AWS CodeCommit. The OAuth connection is per-account, not per-repo.
Step 1: Initialize the Amplify App and Connect Git
Install the Amplify CLI and initialize the project locally. This creates the amplify/ directory with backend category configs and the team-provider-info.json for environment isolation.
# Install Amplify CLI globally
npm install -g @aws-amplify/cli
# Configure CLI with IAM credentials
amplify configure
# Initialize Amplify in your project root
amplify init
During amplify init, you specify the framework (React), source directory, build command, and output directory. Amplify writes an initial amplify.yml based on these answers. Verify the generated file matches your actual build output path — the default assumes build/ for Create React App, but Vite projects output to dist/.
Then connect the repository in the Amplify Console (AWS Console → Amplify → New App → Host Web App) and authorize the Git provider. Select the repository and branch. Amplify detects the amplify.yml if present; otherwise it auto-detects the framework and generates build settings.
Step 2: Configure the Build Spec
The amplify.yml controls every phase of the build. For a React + Amplify backend project, the backend phase must run before the frontend phase so that aws-exports.js is generated before the React build consumes it.
version: 1
backend:
phases:
build:
commands:
- '# Execute Amplify CLI with the helper script'
- amplifyPush --simple
frontend:
phases:
preBuild:
commands:
- npm ci
build:
commands:
- npm run build
artifacts:
baseDirectory: build
files:
- '**/*'
cache:
paths:
- node_modules/**/*
The amplifyPush --simple helper is an Amplify-provided script available in the build environment. It runs amplify push non-interactively using the environment's IAM role. If you skip the backend phase entirely, the build will use whatever aws-exports.js was last committed to the repo — which is often stale or points to the wrong environment.
Step 3: Environment Variables
Set environment variables in the Amplify Console under App Settings → Environment Variables. Variables prefixed with REACT_APP_ are embedded into the React build at compile time via Create React App's build process. They are NOT runtime secrets — they end up in the browser bundle.
# Verify environment variables are accessible in the build
# (Run this in the build commands section for debugging)
env | grep REACT_APP
For secrets that must not reach the browser (API keys, signing secrets), use AWS Secrets Manager or Parameter Store and access them from Lambda functions — not from the React build environment.
Step 4: Branch-Based Environments and Preview URLs
Each connected branch gets its own Amplify environment. Map branches to Amplify backend environments explicitly to avoid multiple branches sharing the same backend resources.
# Create a new Amplify environment for a staging branch
amplify env add
# Follow prompts: name it 'staging', create new IAM role or use existing
# List environments
amplify env list
# Check out the staging environment
amplify env checkout staging
In the Amplify Console, under Branch settings, set the backend environment for each branch. If you connect a staging branch but don't assign it a backend environment, it defaults to the environment of the last push — which is almost certainly wrong in a team workflow.
AppSync / Cognito / DynamoDB"] C --> F["Amplify staging environment
Isolated backend resources"] D --> G["PR Preview URLs
Ephemeral frontend only"] G -.->|"shares"| F E --> H["production.amplifyapp.com"] F --> I["staging.amplifyapp.com"] G --> J["pr-123.amplifyapp.com"]
- main branch maps to the
prodAmplify environment — its own AppSync, Cognito, DynamoDB tables. - staging branch maps to the
stagingenvironment — isolated backend resources, separate data. - feature/* branches can use pull request previews with ephemeral URLs, sharing the staging backend or a dedicated dev environment.
- Each environment has its own
aws-exports.jsgenerated at build time, pointing to the correct endpoints.
Managing Backend APIs with Amplify: What Actually Works
This is where the React + Node.js question gets specific. Amplify's backend management is designed around its own provisioned resources. Here's the honest breakdown.
Option A: Amplify-Managed GraphQL API (AppSync) — Recommended Path
If you're willing to define your data model in Amplify's schema format, the GraphQL API category provisions AppSync with DynamoDB resolvers, handles auth via Cognito, and generates TypeScript types for your frontend. This is the path Amplify Studio is built to support.
# Add a GraphQL API
amplify add api
# Select: GraphQL
# Select: Amazon Cognito User Pool (for auth)
# Select: Single object with fields (to start with a sample schema)
# Edit the schema
# amplify/backend/api//schema.graphql
# Deploy
amplify push
After push, Amplify generates src/graphql/ with queries, mutations, and subscriptions. The Amplify JS library handles request signing with Cognito credentials. You don't write resolver logic — the Amplify transformer generates VTL resolvers from the schema directives (@model, @auth, @hasMany, etc.).
Option B: Amplify-Managed REST API (API Gateway + Lambda)
If your backend logic is in Node.js Lambda functions, Amplify can manage these through the REST API category. This is the closest Amplify gets to managing a Node.js backend.
# Add a REST API backed by Lambda
amplify add api
# Select: REST
# Provide a friendly name for your resource
# Provide a path: /items
# Select: Create a new Lambda function
# Select runtime: NodeJS
# Select template: Serverless ExpressJS function
# Deploy
amplify push
Amplify scaffolds an Express.js app inside amplify/backend/function/<function-name>/src/. This is a real Node.js Express app running inside Lambda via the aws-serverless-express (or @vendia/serverless-express) adapter. Your existing Express route handlers can be moved here with minimal changes — the main adjustment is the entry point wrapping.
Think of it as your Express app running inside a Lambda container instead of a long-lived server process. The routing logic is identical; the process lifecycle is not. Cold starts are real, and connection pooling to RDS behaves differently than on EC2.
Option C: External Node.js Backend (EC2, ECS, Elastic Beanstalk)
If your Node.js backend runs on EC2, ECS, or Elastic Beanstalk, Amplify Hosting does not manage it. Amplify deploys your React frontend; the frontend calls your external API endpoint directly. You configure the API base URL as an environment variable in the Amplify Console.
In this setup, Amplify Studio is not useful for backend management — Studio only surfaces Amplify-provisioned resources. Your CI/CD for the backend is a separate pipeline (CodePipeline, GitHub Actions, etc.).
This is a valid architecture. Amplify Hosting is still valuable for the frontend CI/CD and CDN layer. Just don't expect the Amplify Console to give you visibility into your ECS service health or deployment status.
Amplify Studio: Practical Scope and Limitations
Amplify Studio is accessed via the Amplify Console → Studio. It requires an Amplify-managed backend (created via CLI or directly in Studio). If you connected only a frontend app to Amplify Hosting without running amplify init and adding backend categories, Studio has nothing to surface.
What Studio actually provides in production use:
- Data modeling: Visual schema editor that writes to
schema.graphql. Useful for teams where non-engineers need to modify data models. Changes are committed back to the repo. - UI component generation: Pulls Figma component definitions and generates React components with data binding to your Amplify data model. The generated code is placed in
src/ui-components/and is meant to be used as-is or extended. - User management: Cognito user pool management UI — create, disable, and inspect users without going to the Cognito console directly.
- Content management: If you use the DataStore category, Studio can display and edit records directly.
What Studio does not do: manage Lambda function code, configure API Gateway routes beyond what the CLI provisions, or provide observability into your backend runtime behavior. For that, you're still in CloudWatch and the individual service consoles.
The Misdiagnosis Engineers Hit Most Often
Here's a failure pattern that shows up repeatedly: engineer connects a React app to Amplify Hosting, runs a successful build, opens the deployed URL — and the app crashes immediately with a network error calling the API.
The assumption is that the build failed to include the API endpoint. The console shows a successful deployment. CloudWatch shows nothing in the Lambda logs. The actual cause: aws-exports.js was committed to the repo pointing to a dev environment endpoint, but the Amplify Hosting build was connected to a prod backend environment. The build ran amplify push against prod, generated a new aws-exports.js in the build environment, but the React build had already run npm ci and imported the stale committed version.
The fix is ordering: the backend phase in amplify.yml must complete before the frontend npm run build executes. The amplify.yml structure enforces this — but only if you actually use the backend phase block. Engineers who copy a frontend-only amplify.yml and add amplify push inside the frontend preBuild commands break the ordering guarantee.
Observable symptom: the deployed app's network requests go to the wrong AppSync endpoint or a non-existent one. Check the browser's network tab — the GraphQL endpoint URL in the request will tell you which environment aws-exports.js was pointing to at build time.
IAM Permissions for the Amplify Build Role
Amplify creates a service role for the build environment. This role needs permissions to call CloudFormation, AppSync, Cognito, Lambda, DynamoDB, and S3 on your behalf during amplify push. The default role Amplify creates has broad permissions. For production, scope it down.
# Check the current Amplify service role
aws amplify get-app --app-id YOUR_APP_ID --query 'app.iamServiceRoleArn' --output text
# Review the policies attached to the role
aws iam list-attached-role-policies --role-name YOUR_AMPLIFY_ROLE_NAME
At minimum, the build role needs the following for a standard Amplify backend deployment:
🔽 Click to expand — Amplify Build Role IAM Policy (minimum scope)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"cloudformation:CreateStack",
"cloudformation:UpdateStack",
"cloudformation:DeleteStack",
"cloudformation:DescribeStacks",
"cloudformation:DescribeStackEvents",
"cloudformation:DescribeStackResource",
"cloudformation:GetTemplate",
"cloudformation:ValidateTemplate"
],
"Resource": "arn:aws:cloudformation:us-east-1:123456789012:stack/amplify-*/*"
},
{
"Effect": "Allow",
"Action": [
"s3:CreateBucket",
"s3:PutObject",
"s3:GetObject",
"s3:ListBucket",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::amplify-*",
"arn:aws:s3:::amplify-*/*"
]
},
{
"Effect": "Allow",
"Action": [
"iam:CreateRole",
"iam:AttachRolePolicy",
"iam:PutRolePolicy",
"iam:PassRole",
"iam:GetRole",
"iam:DeleteRole",
"iam:DetachRolePolicy",
"iam:DeleteRolePolicy"
],
"Resource": "arn:aws:iam::123456789012:role/amplify-*"
}
]
}
The actual required actions depend on which Amplify categories you use. Add AppSync, Cognito, Lambda, and DynamoDB permissions as needed. The Amplify documentation lists the minimum permissions per category — verify against the current docs since these change with CLI versions.
Verifying the Deployment End-to-End
After a successful build, verify each layer independently rather than assuming a green build status means a working app.
# Check Amplify app build status
aws amplify list-jobs --app-id YOUR_APP_ID --branch-name main --query 'jobSummaries[0].{status:status,jobId:jobId}'
# Get the deployed URL
aws amplify get-branch --app-id YOUR_APP_ID --branch-name main --query 'branch.displayName'
# Verify the AppSync API endpoint is reachable
aws appsync list-graphql-apis --query 'graphqlApis[?name==`YOUR_API_NAME`].uris'
# Check Lambda function exists and is active
aws lambda get-function --function-name YOUR_FUNCTION_NAME --query 'Configuration.{State:State,LastModified:LastModified}'
If the build succeeded but the app is broken, the issue is almost always in the aws-exports.js content or a backend resource that failed to update during amplify push. Check the CloudFormation stack events for the Amplify backend stacks directly — the Amplify Console build logs often truncate CloudFormation errors.
# List CloudFormation stacks created by Amplify
aws cloudformation list-stacks --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE --query 'StackSummaries[?starts_with(StackName, `amplify-`)].{Name:StackName,Status:StackStatus}'
Wrap-Up and Next Steps for AWS Amplify Full-Stack Deployment
AWS Amplify Hosting gives you a production-grade CI/CD pipeline and CDN for your React frontend with minimal configuration. The backend story is more nuanced: Amplify manages what it provisions through its CLI categories (AppSync, Lambda-backed REST, Cognito), and leaves everything else to you. If your Node.js backend is Express on Lambda, Amplify can own it. If it's a long-running server on EC2 or ECS, Amplify Hosting is still useful for the frontend, but your backend pipeline is separate.
Amplify Studio adds value specifically when your team uses Amplify-provisioned AppSync and Cognito — the visual data modeling and component generation are genuinely useful for accelerating frontend development. It's not a general-purpose backend management console.
The ordering dependency between backend and frontend build phases is the single most common source of silent deployment failures. Get that right in your amplify.yml before anything else.
- Amplify CLI Installation and Configuration
- Amplify Studio Overview
- Amplify CLI IAM Permissions Reference
- Amplify Team Workflows and Environments
Glossary
| Term | Definition |
|---|---|
| Amplify Hosting | Managed CI/CD and CDN service for static and server-side rendered frontends. Backed by CloudFront and S3. |
| Amplify CLI | Command-line toolchain that provisions AWS backend resources (AppSync, Lambda, Cognito, DynamoDB) via CloudFormation. |
| aws-exports.js | Auto-generated configuration file containing endpoint URLs, region, and auth settings for the Amplify JS library. Generated by amplify push. |
| Amplify Environment | An isolated set of backend resources (separate CloudFormation stacks) mapped to a deployment stage such as dev, staging, or prod. |
| Amplify Studio | Visual interface for managing Amplify-provisioned data models, Cognito users, and React component generation from Figma designs. |
Related Posts
- 📄 AWS Cognito for User Login: User Pools vs. Identity Pools Explained
- 📄 How to Trigger Lambda from an API Gateway GET Request (HTTP API Step-by-Step)
- 📄 How to Deploy a React App to S3 and CloudFront Using GitHub Actions
- 📄 How to Deploy a Simple Node.js App with AWS Elastic Beanstalk
- 📄 How to Host a Static Website on S3: Step-by-Step Guide
Comments
Post a Comment