I am using TypeScript and Contentful in the same project and am having issue getting my head around how to map out the API response within an interface in a way that is clean and sensible and dynamic some sort as the API responses from contentful are massive and ugly - and for each different content type they could be different.
So these are my main issues:
All help appreciated, really just need advice down the right way to investigate the solutions.
This is an example of one of the contentful API responses:
{
"sys":{
"type":"Array"
},
"total":2,
"skip":0,
"limit":100,
"items":[
{
"metadata":{
"tags":[
]
},
"sys":{
"space":{
"sys":{
"type":"Link",
"linkType":"Space",
"id":"rikydtrxnc79"
}
},
"id":"1w9AMDdCqRHRMEfzif3J0V",
"type":"Entry",
"createdAt":"2023-06-22T14:17:28.969Z",
"updatedAt":"2023-06-22T14:17:28.969Z",
"environment":{
"sys":{
"id":"master",
"type":"Link",
"linkType":"Environment"
}
},
"revision":1,
"contentType":{
"sys":{
"type":"Link",
"linkType":"ContentType",
"id":"blogPost"
}
},
"locale":"en-US"
},
"fields":{
"title":"Second Test",
"body":{
"data":{
},
"content":[
{
"data":{
},
"content":[
{
"data":{
},
"marks":[
],
"value":"This is a test.",
"nodeType":"text"
}
],
"nodeType":"paragraph"
}
],
"nodeType":"document"
},
"tags":[
{
"sys":{
"type":"Link",
"linkType":"Entry",
"id":"5vuJaOnrVvh34oAiBt8qgh"
}
},
{
"sys":{
"type":"Link",
"linkType":"Entry",
"id":"6feqnd9taR55ruluGBsp8h"
}
}
]
}
},
{
"metadata":{
"tags":[
]
},
"sys":{
"space":{
"sys":{
"type":"Link",
"linkType":"Space",
"id":"rikydtrxnc79"
}
},
"id":"32eWu0P7zXgbxqum5nbdqP",
"type":"Entry",
"createdAt":"2023-06-22T14:14:34.053Z",
"updatedAt":"2023-06-22T14:14:34.053Z",
"environment":{
"sys":{
"id":"master",
"type":"Link",
"linkType":"Environment"
}
},
"revision":1,
"contentType":{
"sys":{
"type":"Link",
"linkType":"ContentType",
"id":"blogPost"
}
},
"locale":"en-US"
},
"fields":{
"title":"Test",
"body":{
"data":{
},
"content":[
{
"data":{
},
"content":[
{
"data":{
},
"marks":[
],
"value":"This is a test",
"nodeType":"text"
}
],
"nodeType":"heading-1"
},
{
"data":{
},
"content":[
{
"data":{
},
"marks":[
],
"value":"",
"nodeType":"text"
}
],
"nodeType":"paragraph"
},
{
"data":{
},
"content":[
{
"data":{
},
"marks":[
],
"value":"This is a test",
"nodeType":"text"
}
],
"nodeType":"heading-3"
},
{
"data":{
},
"content":[
{
"data":{
},
"marks":[
],
"value":"",
"nodeType":"text"
}
],
"nodeType":"paragraph"
},
{
"data":{
},
"content":[
{
"data":{
},
"marks":[
],
"value":"This is a test.",
"nodeType":"text"
}
],
"nodeType":"paragraph"
},
{
"data":{
},
"content":[
{
"data":{
},
"marks":[
],
"value":"",
"nodeType":"text"
}
],
"nodeType":"paragraph"
}
],
"nodeType":"document"
},
"tags":[
{
"sys":{
"type":"Link",
"linkType":"Entry",
"id":"63QnQZsSKUUI3TNAvsI19J"
}
},
{
"sys":{
"type":"Link",
"linkType":"Entry",
"id":"1O7OQ1YFLMshW7BwBTL3ER"
}
}
],
"images":[
{
"sys":{
"type":"Link",
"linkType":"Asset",
"id":"35LgxsKv96DyW51Uu8DrnM"
}
},
{
"sys":{
"type":"Link",
"linkType":"Asset",
"id":"4bxRR1bBIknnVzRjeqvUIr"
}
}
]
}
}
],
"includes":{
"Entry":[
{
"metadata":{
"tags":[
]
},
"sys":{
"space":{
"sys":{
"type":"Link",
"linkType":"Space",
"id":"rikydtrxnc79"
}
},
"id":"1O7OQ1YFLMshW7BwBTL3ER",
"type":"Entry",
"createdAt":"2023-06-22T14:13:23.918Z",
"updatedAt":"2023-06-22T14:13:23.918Z",
"environment":{
"sys":{
"id":"master",
"type":"Link",
"linkType":"Environment"
}
},
"revision":1,
"contentType":{
"sys":{
"type":"Link",
"linkType":"ContentType",
"id":"countryTag"
}
},
"locale":"en-US"
},
"fields":{
"countryTag":"usa"
}
},
{
"metadata":{
"tags":[
]
},
"sys":{
"space":{
"sys":{
"type":"Link",
"linkType":"Space",
"id":"rikydtrxnc79"
}
},
"id":"5vuJaOnrVvh34oAiBt8qgh",
"type":"Entry",
"createdAt":"2023-06-22T10:50:36.561Z",
"updatedAt":"2023-06-22T10:50:36.561Z",
"environment":{
"sys":{
"id":"master",
"type":"Link",
"linkType":"Environment"
}
},
"revision":1,
"contentType":{
"sys":{
"type":"Link",
"linkType":"ContentType",
"id":"countryTag"
}
},
"locale":"en-US"
},
"fields":{
"countryTag":"australia"
}
},
{
"metadata":{
"tags":[
]
},
"sys":{
"space":{
"sys":{
"type":"Link",
"linkType":"Space",
"id":"rikydtrxnc79"
}
},
"id":"63QnQZsSKUUI3TNAvsI19J",
"type":"Entry",
"createdAt":"2023-06-22T14:13:11.295Z",
"updatedAt":"2023-06-22T14:13:11.295Z",
"environment":{
"sys":{
"id":"master",
"type":"Link",
"linkType":"Environment"
}
},
"revision":1,
"contentType":{
"sys":{
"type":"Link",
"linkType":"ContentType",
"id":"regionTag"
}
},
"locale":"en-US"
},
"fields":{
"title":"americas"
}
},
{
"metadata":{
"tags":[
]
},
"sys":{
"space":{
"sys":{
"type":"Link",
"linkType":"Space",
"id":"rikydtrxnc79"
}
},
"id":"6feqnd9taR55ruluGBsp8h",
"type":"Entry",
"createdAt":"2023-06-22T10:41:10.693Z",
"updatedAt":"2023-06-22T10:41:10.693Z",
"environment":{
"sys":{
"id":"master",
"type":"Link",
"linkType":"Environment"
}
},
"revision":1,
"contentType":{
"sys":{
"type":"Link",
"linkType":"ContentType",
"id":"regionTag"
}
},
"locale":"en-US"
},
"fields":{
"title":"apac"
}
}
],
"Asset":[
{
"metadata":{
"tags":[
]
},
"sys":{
"space":{
"sys":{
"type":"Link",
"linkType":"Space",
"id":"rikydtrxnc79"
}
},
"id":"35LgxsKv96DyW51Uu8DrnM",
"type":"Asset",
"createdAt":"2023-06-22T14:14:11.979Z",
"updatedAt":"2023-06-22T14:14:11.979Z",
"environment":{
"sys":{
"id":"master",
"type":"Link",
"linkType":"Environment"
}
},
"revision":1,
"locale":"en-US"
},
"fields":{
"title":"Test1",
"description":"",
"file":{
"url":"//images.ctfassets.net/rikydtrxnc79/35LgxsKv96DyW51Uu8DrnM/d8a11b6dc57a416a143af10b3569a7e0/pexels-photo-3844788.jpeg",
"details":{
"size":83381,
"image":{
"width":500,
"height":667
}
},
"fileName":"pexels-photo-3844788.jpeg",
"contentType":"image/jpeg"
}
}
},
{
"metadata":{
"tags":[
]
},
"sys":{
"space":{
"sys":{
"type":"Link",
"linkType":"Space",
"id":"rikydtrxnc79"
}
},
"id":"4bxRR1bBIknnVzRjeqvUIr",
"type":"Asset",
"createdAt":"2023-06-22T14:14:26.979Z",
"updatedAt":"2023-06-22T14:14:26.979Z",
"environment":{
"sys":{
"id":"master",
"type":"Link",
"linkType":"Environment"
}
},
"revision":1,
"locale":"en-US"
},
"fields":{
"title":"Test2",
"description":"",
"file":{
"url":"//images.ctfassets.net/rikydtrxnc79/4bxRR1bBIknnVzRjeqvUIr/4ffca3cdb4db84b6ee2129cf05f06cdc/premium_photo-1661964088064-dd92eaaa7dcf.jpeg",
"details":{
"size":46936,
"image":{
"width":1000,
"height":714
}
},
"fileName":"premium_photo-1661964088064-dd92eaaa7dcf.jpeg",
"contentType":"image/jpeg"
}
}
}
]
}
}
Now the items array and includes array could have different data sets in the objects so this is why I am finding it hard to wrap my head around how to solve the issue of new content types and existing interfaces/schemas not working for them.
I have already mapped it out the response like this but think its excessive?
export interface ContentfulApiResponse {
sys: {
type: string;
};
total: number;
skip: number;
limit: number;
items: [
{
metadata: {
tags: [];
};
sys: {
space: {
sys: {
type: string;
linkType: string;
id: string;
};
};
id: string;
type: string;
createdAt: string;
updatedAt: string;
environment: {
sys: {
id: string;
type: string;
linkType: string;
};
};
revision: number;
contentType: {
sys: {
type: string;
linkType: string;
id: string;
};
};
locale: string;
};
fields: {
title: string;
body: {
data: {};
content: [
{
data: {};
content: [
{
data: {};
marks: [];
value: string;
nodeType: string;
}
];
nodeType: string;
}
];
nodeType: string;
};
tags: [
{
sys: {
type: string;
linkType: string;
id: string;
};
},
{
sys: {
type: string;
linkType: string;
id: string;
};
}
];
};
},
{
metadata: {
tags: [];
};
sys: {
space: {
sys: {
type: string;
linkType: string;
id: string;
};
};
id: string;
type: string;
createdAt: string;
updatedAt: string;
environment: {
sys: {
id: string;
type: string;
linkType: string;
};
};
revision: number;
contentType: {
sys: {
type: string;
linkType: string;
id: string;
};
};
locale: string;
};
fields: {
title: string;
body: {
data: {};
content: [
{
data: {};
content: [
{
data: {};
marks: [];
value: string;
nodeType: string;
}
];
nodeType: string;
},
{
data: {};
content: [
{
data: {};
marks: [];
value: string;
nodeType: string;
}
];
nodeType: string;
},
{
data: {};
content: [
{
data: {};
marks: [];
value: string;
nodeType: string;
}
];
nodeType: string;
},
{
data: {};
content: [
{
data: {};
marks: [];
value: "";
nodeType: string;
}
];
nodeType: string;
},
{
data: {};
content: [
{
data: {};
marks: [];
value: string;
nodeType: string;
}
];
nodeType: string;
},
{
data: {};
content: [
{
data: {};
marks: [];
value: string;
nodeType: string;
}
];
nodeType: string;
}
];
nodeType: string;
};
tags: [
{
sys: {
type: string;
linkType: string;
id: string;
};
},
{
sys: {
type: string;
linkType: string;
id: string;
};
}
];
images: [
{
sys: {
type: string;
linkType: string;
id: string;
};
},
{
sys: {
type: string;
linkType: string;
id: string;
};
}
];
};
}
];
includes: {
Entry: ContentfulIncludesEntry[];
Asset: ContentfulIncludesAsset[];
};
}
interface ContentfulIncludesAsset {
metadata: {
tags: [];
};
sys: {
space: {
sys: {
type: string;
linkType: string;
id: string;
};
};
id: string;
type: string;
createdAt: string;
updatedAt: string;
environment: {
sys: {
id: string;
type: string;
linkType: string;
};
};
revision: number;
locale: string;
};
fields: {
title: string;
description: string;
file: {
url: string;
details: {
size: number;
image: {
width: number;
height: number;
};
};
fileName: string;
contentType: string;
};
};
}
interface ContentfulIncludesEntry {
metadata: {
tags: [];
};
sys: {
space: {
sys: {
type: string;
linkType: string;
id: string;
};
};
id: string;
type: string;
createdAt: string;
updatedAt: string;
environment: {
sys: {
id: string;
type: string;
linkType: string;
};
};
revision: number;
contentType: {
sys: {
type: string;
linkType: string;
id: string;
};
};
locale: string;
};
fields: {
countryTag?: string;
regionTag?: string;
};
}
And bear in mind the fields object can change with every content type.
My first thought is that you should start by creating a common type for each property name. Sys, Metadata, Tag, Environment, etc, completely ignoring the fact that they'll have circular references that might prevent them from compiling because in this phase you just want to get it all down and understand it. For example:
interface Item {
metadata?: Metadata;
sys?: Sys;
fields?: Fields;
}
Another idea is to not even try to strongly type the server response. Instead, accept that the response is unknown
and then use zod to assert the data into the types your code expects. An advantage to this is that run-time assertions will help you find bugs and surprises in ways that unit tests and static type checking simply can't.
However, are you sure you need to do any of this yourself? If something like @bgschiller/contentful-typescript-codegen doesn't help, I'd expect you could find something you could use out there?
(Note: I'm not recommending @bgschiller/contentful-typescript-codegen. I'd never heard of it before and only found it by searching for "types/contentful" on npmjs.com.)