Filter concepts by levelShowing all levels.

AWS · Full-Stack Frontends

AWS for Full-Stack / React Applications

Files or a server, one origin instead of two, and a request path you can narrow a failure down inside

  • Private bucket, alwaysCloudFront reaches it with OAC
  • Certificate in us-east-1required for viewer HTTPS on CloudFront
  • One origin, no CORS/api/* is a behavior, not a domain
  • Two paths, one domainshell from the edge, data through the ALB

1.Static Output or Server Rendering

A React build produces either a folder of files or a server. If it is a folder — plain React, or Next.js exported statically — it goes in a private S3 bucket behind CloudFront and there is nothing to run. If the app renders pages per request, something has to execute that code: a container behind a load balancer, or a function at the edge. Which one you have is decided by the framework configuration, not by preference.

Key features

  • Static output means no compute to patch, scale, or pay for while idle — CloudFront serves files from S3
  • The S3 bucket stays private; CloudFront reaches it with origin access control (OAC), which AWS recommends over the legacy origin access identity
  • Using OAC with an S3 bucket origin requires S3 Object Ownership set to Bucket owner enforced, the default for new buckets
  • A bucket configured as an S3 website endpoint must be attached as a custom origin, and cannot use OAC or OAI at all
  • Client-side routing needs CloudFront to answer 403 and 404 from the origin with `/index.html` and a 200, or deep links break on refresh
  • Server rendering means a running process: ECS or Lambda, with cold starts, a deployment, and a scaling policy to own

What each rendering mode actually needs from AWS

What each rendering mode actually needs from AWS
ModeRuns atAWS shapeWatch for
Client-side rendered SPABuild time onlyS3 + CloudFront, OAC, SPA error responsesDeep links 404 without the error-response mapping
Statically exported pagesBuild time onlyS3 + CloudFrontContent changes need a rebuild, not just a cache purge
Server-side rendered pagesEvery requestECS behind an ALB, or LambdaCold starts, and a runtime that now needs secrets and IAM
Incremental regenerationBuild time, then in the backgroundCompute plus shared storage for the regenerated pagesTwo tasks regenerating the same page independently
API routesEvery requestSame compute as SSR, or a separate serviceThey are a backend — they need the same review as one
Two builds, two operational bills

Static output — S3 + CloudFront

  • The build produces files; nothing executes at request time
  • CloudFront serves from edge caches, origin is rarely touched
  • Scales with no configuration and costs nothing while idle
  • Deploy is a sync plus an invalidation of the changed paths
  • Cannot do per-request server logic, secrets, or SSR data fetching

Server rendering — ECS or Lambda

  • The build produces a server; code runs on every uncached request
  • Needed for SSR data fetching, per-user pages, server actions
  • Has cold starts (Lambda) or a warm fleet to pay for (ECS)
  • Deploy is a rolling release with health checks and rollback
  • Still put CloudFront in front, and still serve assets from S3
  • Static output — S3 + CloudFront
    • The build produces files; nothing executes at request time
    • CloudFront serves from edge caches, origin is rarely touched
    • Scales with no configuration and costs nothing while idle
    • Deploy is a sync plus an invalidation of the changed paths
    • Cannot do per-request server logic, secrets, or SSR data fetching
  • Server rendering — ECS or Lambda
    • The build produces a server; code runs on every uncached request
    • Needed for SSR data fetching, per-user pages, server actions
    • Has cold starts (Lambda) or a warm fleet to pay for (ECS)
    • Deploy is a rolling release with health checks and rollback
    • Still put CloudFront in front, and still serve assets from S3

Signature

files → S3 (private) + CloudFront + OAC · server → ECS/Lambda behind CloudFront · SPA → 403/404 ⇒ /index.html 200

If the build produces files, ship them from a private S3 bucket behind CloudFront with OAC, and map 403/404 to /index.html with a 200 so deep links work. If it produces a server, run it on ECS or Lambda — but keep static assets on the S3 origin, and only render the routes that actually need rendering.

2.DNS, TLS, Caching, CORS, and Environments

Between a built frontend and a working site sits a short list of configuration that is easy to get wrong once and then never think about again: which name resolves where, which certificate is valid, what gets cached and for how long, whether the browser is allowed to call the API, and how staging is kept apart from production. Each of these has one common failure and one correct answer.

Key features

  • To use an ACM certificate for HTTPS between viewers and CloudFront, request or import it in US East (N. Virginia) — us-east-1
  • A certificate for HTTPS between CloudFront and an ALB origin can be requested in any Region
  • A distribution's alternate domain name must be covered exactly, or by a same-level wildcard, in the certificate's subject alternative name field
  • Route 53 alias records point at CloudFront and ALB without an extra lookup, and unlike CNAME they can be used at the zone apex
  • Cache hashed asset filenames for a year as immutable and never invalidate them; invalidate only the unhashed entry points such as `/index.html`
  • Same-origin APIs (a `/api/*` behavior on the same distribution) mean no CORS preflight and no cross-site cookie flags at all

The seven pieces, the usual mistake, and the setting that fixes it

The seven pieces, the usual mistake, and the setting that fixes it
PieceUsual mistakeWhat to do instead
DNSCNAME at the zone apex, which is not validRoute 53 alias record — works at the apex and needs no second lookup
TLS certificateCertificate requested in the workload's RegionRequest or import it in us-east-1 for CloudFront; any Region for an ALB origin
Alternate domain nameName not present in the certificate's SAN fieldExact match, or a wildcard at the same level as the name being added
CDN cachingOne TTL for everything, or caching disabled everywhereLong and immutable for hashed assets, none for API paths, short for the HTML entry point
InvalidationInvalidating `/*` on every deployInvalidate the entry point only; hashed filenames make the rest unnecessary
CORSAPI on a second domain, then debugging preflightsServe the API as a `/api/*` behavior on the same distribution
Environment separationOne account, names distinguished by a prefixSeparate accounts per environment, separate zones, separate distributions

Same-origin API versus a separate API domain

Same-origin API versus a separate API domain
Aspect/api/* behavior, same domainapi.example.com, separate domain
Preflight requestsNone — it is same-originOPTIONS before most non-simple requests
CookiesFirst-party, plain `Secure; HttpOnly`Cross-site: needs `SameSite=None`, and is subject to browser tracking rules
CertificatesOne certificate, one distributionTwo names to cover and renew
Local developmentDev server proxy mirrors the production path splitDev and production differ in origin, so CORS bugs appear only in production
Reach for it whenThe default for a first-party frontend and APIThe API is a genuinely separate public product with its own consumers
One domain, two behaviors — and where each piece of configuration lives

Everything the browser talks to is one origin, so there is no CORS preflight and no cross-site cookie. The split into two backends happens inside CloudFront, invisibly to the browser.

  • A diagram showing a browser making two kinds of request to a single domain.
  • Route 53 holds an alias record for app.example.com pointing at one CloudFront distribution. The ACM certificate for that name must live in us-east-1.
  • Inside the distribution, two cache behaviors: the path pattern /api/* forwards to an ALB origin with caching disabled, and the default path pattern serves the private S3 bucket via origin access control with long caching.
  • A note beneath the assets path: hashed filenames are cached for a year and never invalidated; only index.html is invalidated on deploy.
  • A note beneath the API path: because both share one origin name, the browser sends no preflight and cookies are first-party.

Signature

alias record · ACM in us-east-1 · hashed assets immutable · invalidate /index.html only · /api/* same origin

Alias records at the apex, ACM certificate in us-east-1 for CloudFront, hashed assets cached for a year and never invalidated, only the entry point invalidated on deploy, the API served as a same-origin `/api/*` behavior so CORS never arises, and one account per environment.

3.The Full Request Path, End to End

A full-stack design is one path drawn twice: once for loading the application, and once for the calls it makes afterwards. Both start at the same domain and diverge at CloudFront — the first is answered from cache or S3, the second travels to the load balancer, the backend, and the database. Being able to draw both, and name what fails at each hop, is the whole skill.

Key features

  • The first request loads the app shell; every later request is data — they take different paths through the same distribution
  • Authentication happens on the data path, not the asset path: the app shell is public, and the API decides what the user may see
  • A token in `localStorage` is readable by any script on the page; an HttpOnly cookie is not, and is first-party if the API is same-origin
  • CloudFront must forward the `Authorization` header for API behaviors, which means those behaviors have caching disabled
  • The request carries an identifier from the edge inward, so a user-reported failure can be found in the backend logs
  • The database is reached only by the backend, from a private subnet, using a security group that names the application's group

Every hop, what it does, and what its failure looks like

Every hop, what it does, and what its failure looks like
HopIts jobWhat failure looks like
Route 53Answer the name with the distributionSite unreachable everywhere at once; recent record change is the first suspect
CloudFrontServe assets, forward API calls, terminate TLSCertificate warning, or an old build served after a deploy
S3 origin (OAC)Hold the built files, privately403 on every asset — usually the bucket policy or the OAC signing setting
ALBRoute to a healthy backend target503 with zero healthy targets; check the health check first
BackendAuthorize, apply logic, query401 or 403 on a valid session, or a 500 in application logs
Connection poolReuse database connectionsRequests queue and time out while the database itself is idle
RDSAnswer the querySlow queries in Performance Insights; latency shows up first at the backend

Where the session lives, and what each choice costs

Where the session lives, and what each choice costs
PropertyHttpOnly cookie (same-origin API)Token in localStorage
Readable by page scriptsNoYes — any injected script can take it
Sent automaticallyYes, by the browserNo — the app must attach it to every call
Cross-site flags neededNo, if the API is same-originNot applicable
CSRF exposureYes — needs a token or SameSite protectionNo, because nothing is sent automatically
Works with a static frontendYesYes
Default choiceCookie, with a same-origin API pathOnly when the API is genuinely cross-origin and third-party
Load the app, then call the API — two paths, one domain
Browser
Route 53
CloudFront
ALB
Backend
RDS
  1. 1resolve app.example.comAlias record answers with the distribution
  2. 2CloudFront address
  3. 3GET / (the app shell)
  4. 4index.html + hashed assetsServed from the edge cache or the private S3 origin
  5. 5GET /api/orders + cookie or bearer tokenSame origin, so no preflight
  6. 6forward, caching disabledAuthorization header forwarded
  7. 7route by listener rule to a healthy target
  8. 8query, from a pooled connectionPrivate subnet; sg-rds allows sg-app only
  9. 9rows
  10. 10200 with JSON, and the request id
  1. Browser → Route 53: resolve app.example.com (Alias record answers with the distribution)
  2. Route 53 → Browser: CloudFront address
  3. Browser → CloudFront: GET / (the app shell)
  4. CloudFront → Browser: index.html + hashed assets (Served from the edge cache or the private S3 origin)
  5. Browser → CloudFront: GET /api/orders + cookie or bearer token (Same origin, so no preflight)
  6. CloudFront → ALB: forward, caching disabled (Authorization header forwarded)
  7. ALB → Backend: route by listener rule to a healthy target
  8. Backend → RDS: query, from a pooled connection (Private subnet; sg-rds allows sg-app only)
  9. RDS → Backend: rows
  10. Backend → Browser: 200 with JSON, and the request id

Signature

Browser → Route 53 → CloudFront → (S3 assets | ALB → backend → RDS)

Draw the path twice: shell from the edge, data through the ALB to the backend and database. Keep both on one origin so there is no CORS, disable caching on authenticated behaviors and forward the Authorization header, and make sure every hop emits something so a failure can be narrowed to one of them.

Remember

  • A SPA needs CloudFront to answer 403 and 404 with /index.html and a 200, or deep links break on refresh.
  • A bucket configured as an S3 website endpoint must be a custom origin and cannot use OAC or OAI — which is what forces it to be public.
  • Hashed asset filenames never need invalidating; invalidate the entry point only, not /*.
  • Never cache a behavior that carries a session — a cache key without the credential serves one user's data to another.

Pro tip

  • Use a relative /api base path in the frontend so one built artifact is promotable across environments; anything injected at build time is fixed in the bundle and public anyway.
  • Serving the API as a behavior on the same distribution removes CORS preflights, cross-site cookie flags, and a category of bug that only reproduces in production.

Next up

  • AWS Disaster and Failure Testing
  • Production Debugging on AWS
  • CloudFront and Edge Delivery

Sources

  • Restrict access to an Amazon S3 origin — docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html
  • Requirements for using SSL/TLS certificates with CloudFront — docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cnames-and-https-requirements.html
  • Checked against official AWS documentation 2026-09-04

Spotted a mistake on this page? Tell us.