Godrej Design Lab

Querying Content from Strapi

Strapi provides both a REST and a GraphQL API for interacting with it. In this chapter, page, we'll explore the REST API, its limitations and how we've worked around them for this project.

GET Document

Documents are identified by their documentId attribute. In prior versions of Strapi, documents were identified by their numeric id attribute. This is no longer the case.

The URL syntax for retrieving a single document is:

GET /api/{collection-type}/{documentId}

For example, to retrieve a Page Context document, make a GET request to:

GET /api/page-contexts/paa9ijdf7jaj7286jcjueexi
Response: GET /api/page-contexts/paa9ijdf7jaj7286jcjueexi
{
"data": {
"id": 1,
"documentId": "paa9ijdf7jaj7286jcjueexi",
"name": "Default",
"site_title": "Godrej Design Lab",
"site_description": "It's a design lab.",
"createdAt": "2025-07-18T21:16:50.356Z",
"updatedAt": "2025-09-06T11:09:09.622Z",
"publishedAt": "2025-09-06T11:09:09.526Z",
"blurb": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"text": "Godrej Design Lab",
"bold": true
},
{
"type": "text",
"text": " is a platform which encourages and advances design excellence and exploration."
}
]
}
]
},
"meta": {
}
}

As you can see, not all the attributes are returned. The following are nowhere to be seen:

  • cover
  • navigation
  • featured_links
  • arbitrary_code

The populate parameter

By default, the REST API does not populate any relations, media fields, components, or dynamic zones. There is a populate parameter that can be used to include and expand such fields in the query results.

Let's try again with the populate parameter:

GET /api/page-contexts/paa9ijdf7jaj7286jcjueexi?populate=*
Response: GET /api/page-contexts/paa9ijdf7jaj7286jcjueexi?populate=*
{
"data": {
"id": 1,
"documentId": "paa9ijdf7jaj7286jcjueexi",
"name": "Default",
"site_title": "Godrej Design Lab",
"site_description": "It's a design lab.",
"createdAt": "2025-07-18T21:16:50.356Z",
"updatedAt": "2025-09-06T11:09:09.622Z",
"publishedAt": "2025-09-06T11:09:09.526Z",
"blurb": [
{
// same as before
}
],
"cover": {
"id": 12,
"createdAt": "2025-07-19T06:50:15.034Z",
"updatedAt": "2025-07-19T06:50:15.034Z",
"publishedAt": "2025-07-19T06:50:15.036Z",
"documentId": "thw6g0mmp2u1dcx7tkhodzde",
"provider": "local",
"provider_metadata": null,
"name": "about-fellowship-2.jpg",
"hash": "about_fellowship_2_38b21dd13b",
"ext": ".jpg",
"mime": "image/jpeg",
"size": 258.52,
"url": "/uploads/about_fellowship_2_38b21dd13b.jpg",
"previewUrl": null,
"alternativeText": null,
"caption": null,
"width": 2000,
"height": 1333,
"formats": {
"thumbnail": {
// ...
},
"xs": {
"name": "xs_about-fellowship-2.jpg",
"hash": "xs_about_fellowship_2_38b21dd13b",
"ext": ".jpg",
"mime": "image/jpeg",
"path": null,
"width": 360,
"height": 240,
"size": 19.89,
"sizeInBytes": 19890,
"url": "/uploads/xs_about_fellowship_2_38b21dd13b.jpg"
},
"sm": {
// ...
},
"xxs": {
// ...
},
"lg": {
// ...
},
"md": {
// ...
},
"xl": {
// ...
}
}
},
"navigation": [
{ "id": 1, "label": "About", "url": "/about-godrej-design-lab" },
// ...
],
"featured_links": [
{ "id": 7, "label": "North: Bhusan Rahul", "url": "/north-bhusan-rahul" },
// ...
],
"arbitrary_code": { "id": 1 }
},
"meta": { }
}

As you can see, it still isn't all there. The arbitrary_code attribute, while present, is vacant.

This is because the populate parameter is conservative by default. It does not infinitely recurse through each of the attributes' children.

This request will return a fully-populated response:

GET /api/page-contexts/paa9ijdf7jaj7286jcjueexi?populate[cover][populate]=*&populate[navigation][populate]=*&populate[featured_links][populate]=*&populate[arbitrary_code][populate]=*
Response: GET /api/page-contexts/paa9ijdf7jaj7286jcjueexi?populate[cover][populate]=*&populate[navigation][populate]=*&populate[featured_links][populate]=*&populate[arbitrary_code][populate]=*
{
"data": {
"id": 1,
"documentId": "paa9ijdf7jaj7286jcjueexi",
"name": "Default",
"site_title": "Godrej Design Lab",
"site_description": "It's a design lab.",
"createdAt": "2025-07-18T21:16:50.356Z",
"updatedAt": "2025-09-06T11:09:09.622Z",
"publishedAt": "2025-09-06T11:09:09.526Z",
"blurb": [
{
// same as before
}
],
"cover": {
// same as before
},
"navigation": [
// same as before
],
"featured_links": [
// same as before
],
"arbitrary_code": {
"id": 1,
"before_head_closing": [
{
"__component": "code.script-v1",
"id": 1,
"type": "text/javascript",
"src": "https://www.googletagmanager.com/gtag/js?id=A_TAG_ID",
"code": null,
"async": true,
"defer": null,
"cross_origin": "anonymous",
"integrity": null,
"nonce": null,
"referrer_policy": "strict-origin-when-cross-origin"
},
{
"__component": "code.script-v1",
"id": 2,
"type": "text/javascript",
"src": null,
"code": "window.dataLayer = window.dataLayer || [ ]\nfunction gtag () {\n\tdataLayer.push( arguments )\n}\ngtag( \"js\", new Date() )\ngtag( \"config\", \"A_TAG_ID\" )",
"async": null,
"defer": null,
"cross_origin": "anonymous",
"integrity": null,
"nonce": null,
"referrer_policy": "strict-origin-when-cross-origin"
}
],
"after_body_opening": [ ],
"before_body_closing": [ ]
}
},
"meta": {
}
}

In the request URL, we had explicitly mention every attribute that we wanted to populate. For the Page Context content type, this is as long as it gets. For Posts and Pages however, the level of nesting is even deeper and the resulting URL ends up being way longer than what servers can conceivably handle.

Issues with Strapi's REST API

There are two issues that we face when using Strapi's REST API:

  1. We cannot query a document by its URL slug
  2. The API query URLs for the Post and Page content types is longer than what servers can handle

Querying a document by its slug

When users visit the URL /about-godrej-design-lab, we have to fetch the About page from Strapi. However, we do not have the ID of the document. And Strapi does not provide a convenient way to query a document by a specific attribute.

Enter the Webtools plugin. This plugin solves the above issue while also providing these features:

  • Auto-generates a slug based on the document title
  • Ensures the slugs are unique across the site
  • Can generate a slug based on a specifed URL pattern
  • Provides an API that supports querying documents by their slugs

Once the plugin is set up, accessing the About page looks like:

GET /api/webtools/router?path=/about-godrej-design-lab

instead of (with Strapi's native REST API):

GET /api/pages/ax6e0bjl35ocxzjrrmwik3kn

We demonstrated the above example with the Page content type, and not the Page Context type. This is because a page context document is not something that is ever accessed directly by the end user. It is only accessed indirectly via posts and pages.

Configuring the Webtools plugin

On the admin dashboard, under Webtools -> URL Patterns, ensure the following entries are present:

Content TypePattern
Page/[title]
Post/posts/[title]

Then under Settings -> Users & Permissions plugin -> Roles -> Public -> Permssions -> Webtools, ensure the following is set:

CategoryPermissionValue
CorerouterYes
URL-AliasfindNo

Handling long API query URLs

Addressing this issue is trickier. Including the populate parameters in the query string exceeds the URL length limit, preventing servers from even handling the request.

However, the body of an HTTP request can accommodate considerably large payloads. So that's what we did. We "patched" the Webtools plugin (not Strapi) to enable support for accepting the populate parameters (serialized as JSON) via the request body instead.

You can find the patch under /patches/strapi-plugin-webtools@<version>.patch (<version> refers to the exact version of the plugin, which as of this writing is 1.4.1).

So now, we simply make a POST request to:

POST /api/webtools/router?path=/about-godrej-design-lab
Body: "{"populate":"..."}"

and the entire About page along with all its deeply nested attributes are returned.


On this project

So here's how things play out when a user visits /about-godrej-design-lab:

  1. Request from user's browser hits the frontend server
  2. The frontend server handles this request, invoking the fetch_route_from_cms function as part of the CMS route handler (found at ./apps/frontend-website/src/cms/cms-route-handler.tsx)
  3. The fetch_route_from_cms function preps and makes an HTTP POST request out to the Webtools plugin's API endpoint (hosted on the Strapi server)
  4. The Strapi server returns a response to the frontend server
  5. The frontend server constructs the UI/markup and responds back to user's browser