Skip to content

AEM Headless Tutorial: From Content Fragments to a React App

A step-by-step AEM headless tutorial for AEM as a Cloud Service: configuration, Content Fragment Models, fragments, a GraphQL endpoint, persisted queries and a React client.

Written in October 2026 for AEM as a Cloud Service, following Adobe's "Getting Started with AEM Headless" tutorial and checked against its current documentation.

On this page
  1. Before you start: is headless the right choice?
  2. What you need
  3. Step 1. Create a project configuration
  4. Step 2. Model the content
  5. Step 3. Author the fragments
  6. Step 4. Create a GraphQL endpoint and try a query
  7. Step 5. Save the queries as persisted queries
  8. Step 6. Connect a React app
  9. Step 7. Get ready for production
  10. The REST alternative: Content Fragment Delivery with OpenAPI
  11. Developing locally
  12. Common questions

Headless AEM means that AEM stores and manages the content, and a separate application renders it. Authors create structured content as Content Fragments, and a React, Next.js or mobile app reads it as JSON through an API. This tutorial builds the whole chain on AEM as a Cloud Service, in the same order Adobe's own getting-started tutorial uses: configuration, models, fragments, a GraphQL endpoint, persisted queries, and a small React app.

Before you start: is headless the right choice?

Adobe now recommends that new AEM sites start with Edge Delivery Services. Headless is the pattern for a site or app that already has its own front end, such as a React or Next.js application, a mobile app, or a server that consumes content. If that is your case, read on.

Three related terms come up in Adobe's documentation:

  • Headless: AEM delivers only content, through the GraphQL API or the Content Fragment Delivery OpenAPI, and the application handles all layout.
  • Hybrid: the application is still separate, but authors edit it visually. Adobe's tool for this is now the Universal Editor. The older SPA Editor has been deprecated for new projects since release 2025.1.0.
  • Edge Delivery Services: AEM's own fast "head", which renders pages at the edge. It can also publish Content Fragments as HTML pages.

What you need

  • An AEM as a Cloud Service environment with administrator access. The steps also work on AEM 6.5, with the differences noted below.
  • Node.js and Git on your machine, and a code editor. Adobe's tutorial lists Node.js 18.
  • Basic knowledge of HTML and JavaScript.

Step 1. Create a project configuration

  1. Go to Tools > General > Configuration Browser and click Create.
  2. Enter the title My Project and the name my-project. Use lowercase and hyphens: the name becomes part of the API URLs.
  3. Tick Content Fragment Models and GraphQL Persisted Queries, then click Create.

Step 2. Model the content

Go to Tools > General > Content Fragment Models, open My Project and create two models. Adobe's tutorial uses a team of people:

ModelFields
PersonfullName (single-line text, required), biographyText (multi-line rich text), profilePicture (content reference, images only), occupation (enumeration shown as checkboxes)
Teamtitle (required), shortName (required, unique), description (multi-line rich text), teamMembers (fragment reference, multiple, allowed model: Person)

Each field's property name becomes the key in the JSON your app receives, so choose names you are happy to code against. Publish the configuration and both models.

A tip for new models: since release 2025.2.0 AEM as a Cloud Service has UUID-based reference fields and ...ById queries. An ID survives a fragment being moved or renamed; a path does not.

Step 3. Author the fragments

In Assets, create a folder for the project, for example /content/dam/my-project/en, and allow your models on it. Create a few Person fragments, upload a profile picture for each, then create a Team fragment and add the people to its teamMembers field. More on authoring, models and variations in Content Fragments in AEM.

Step 4. Create a GraphQL endpoint and try a query

  1. Go to Tools > General > GraphQL, click Create, name the endpoint My Project Endpoint and choose the My Project schema. Publish it.
  2. Open Tools > General > GraphQL Query Editor (GraphiQL) and select the endpoint.
  3. Run a query. This one finds a person by name:
query personByName($name: String!) {
  personList(filter: {
    fullName: { _expressions: [{ value: $name, _operator: EQUALS }] }
  }) {
    items {
      _path
      fullName
      occupation
      biographyText { json }
      profilePicture { ... on ImageRef { _path width height } }
    }
  }
}

Set the variable in the Query Variables pane, for example { "name": "Alison Smith" }. The Documentation Explorer on the right shows the schema AEM generated from your models: personList, personByPath, teamList and so on. The API itself is explained in AEM GraphQL API.

Step 5. Save the queries as persisted queries

In GraphiQL, click Save As and save the query above as person-by-name, and a query for all teams as all-teams. Publish both. Copy URL gives you the address the app will call:

/graphql/execute.json/my-project/person-by-name;name=Alison%20Smith

Persisted queries are called with GET, so the CDN can cache them. On the publish service, Adobe documents a default CDN cache time of two hours and a browser cache time of 60 seconds, and both can be changed per query. A production app should use persisted queries only.

Step 6. Connect a React app

Adobe's sample app lives in the adobe/aem-guides-wknd-graphql repository on GitHub, in the basic-tutorial folder:

git clone https://github.com/adobe/aem-guides-wknd-graphql.git
cd aem-guides-wknd-graphql/basic-tutorial
npm install
npm start

Point it at AEM in .env.development. REACT_APP_HOST_URI is your AEM host, REACT_APP_GRAPHQL_ENDPOINT the endpoint, and REACT_APP_AUTH_METHOD is empty for the anonymous publish service or basic, dev-token or service-token for author. The app calls AEM through Adobe's JavaScript client:

import AEMHeadless from '@adobe/aem-headless-client-js';

const client = new AEMHeadless({
  serviceURL: process.env.REACT_APP_HOST_URI,
  endpoint: process.env.REACT_APP_GRAPHQL_ENDPOINT
});

const { data } = await client.runPersistedQuery('my-project/all-teams');
const teams = data.teamList.items;

In development the sample proxies /content and /graphql through its own server, which is why it needs no CORS setup. The sample has not been updated since 2023: it still uses Create React App and version 3 of the client, while the client is at version 4. The AEM calls are the same in a newer React or Next.js setup.

Step 7. Get ready for production

  • Point the app at publish, using a host like https://publish-p123-e456.adobeaemcloud.com, through the CDN. A preview build of the app can read from the preview service, which shows content published to Preview before it goes live.
  • Add a CORS policy as an OSGi configuration in your project's code, deployed through Cloud Manager, listing your app's origin and the paths /graphql/execute.json/.* and the endpoint.
  • Lock down what is public. On publish, content behind a GraphQL endpoint is readable by anonymous users. Restrict folders with closed user groups and set ACLs on the endpoint.
  • Ship the persisted queries to each environment, by publishing them from GraphiQL or as a package of /conf/my-project/settings/graphql/persistentQueries.
  • Use web-optimized images. Ask for _dynamicUrl with _assetTransform in the query and prefix the AEM host. It works on AEM as a Cloud Service and on Rapid Development Environments, not on the local SDK or AEM 6.5.
  • Cache well. Adobe's go-live guidance: a CDN cache time above 600 seconds, responses under about 15 KB of gzipped JSON, and no more than five levels of nested fragments.

The REST alternative: Content Fragment Delivery with OpenAPI

Since release 2025.1.0, AEM as a Cloud Service also delivers fragments through a REST API. A request like GET /adobe/contentFragments?path=/content/dam/my-project/en/teams lists fragments, and GET /adobe/contentFragments/{id}?references=direct-hydrated returns one with its references resolved. The API is switched on through an Adobe Support request and runs on the preview and publish services. Its cache times are fixed: five minutes in the browser and one hour at the CDN. Adobe documents a limit of 200 new requests per second per environment. Choose it when a fixed JSON shape suits the app; choose GraphQL when the app should pick its own fields.

Developing locally

The AEM SDK runs author on localhost:4502 and publish on localhost:4503, and it is fine for models, fragments and GraphQL queries. Some cloud features are missing locally, web-optimized image delivery among them. Every program also gets a Rapid Development Environment, a cloud environment you deploy to in seconds with aio aem:rde:install. It has author and publish services but no preview service.

Common questions

Does this work on AEM 6.5?

Adobe's tutorial is written for both AEM 6.5 and AEM as a Cloud Service. On 6.5, GraphiQL is installed as a package, and some newer features, such as web-optimized image URLs, are not available.

Can authors still edit a headless app visually?

Yes, with the Universal Editor, which Adobe recommends for visual editing of headless apps. The SPA Editor is deprecated for new projects.

Where should the app read content from in production?

From the publish service, through the CDN, using persisted queries. Author is for authoring and for preview builds that need authentication.

Related: AEM GraphQL API, Content Fragments in AEM, AEM as a Cloud Service and AEM architecture.

More in Content Fragments and headless