Static Output or Server Rendering
coreintermediateA 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.
Think of it as
Ask one question of every route: can this page be built before anyone asks for it? If yes for all of them, you have a static site and no compute at all. If no for some of them, you have a server, and the static path is still worth keeping for everything else — most applications are mostly static with a few dynamic routes.
What we're doing: Serve a single-page application from a private bucket so that deep links do not 404.
- 1
- This is the first thing that breaks on every SPA deployment, and it is not a bug in the app.
- 8
- Mapping to 200 rather than passing the error through is what stops search engines and monitors treating every page as broken.
- 16
- AWS recommends OAC over the legacy OAI, and OAC needs Object Ownership set to bucket owner enforced — the default for new buckets.
Why this works: Serving a SPA is two decisions that are easy to get half-right: the bucket must stay private and reachable only through CloudFront, and CloudFront must answer unknown paths with the app shell. Making the bucket public "so the links work" solves the second by abandoning the first, and it is the single most common way a static site becomes a data-exposure incident.
Making the bucket a public website endpoint to get SPA routing
Wrong
Better
What you see: The bucket is readable directly over the internet, bypassing CloudFront entirely — so WAF rules, signed URLs, logging and cache policies all apply to a path nobody is required to use.
Why: A bucket configured as a website endpoint has to be attached to CloudFront as a custom origin, which means it cannot use OAC or OAI and must therefore be public. Every control you attach to the distribution is then optional for an attacker who addresses the bucket directly.
- 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
What each rendering mode actually needs from AWS
Together
Remember: 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.
See also: s3 and alb origin patterns · public access risks · dns tls caching and cors · three tier web application

