Web development keeps changing, and it's all about building fast, reliable websites and apps. That's why many developers use tools like Strapi, a headless CMS, and Next.js.
But there's a way to make these tools work even better together: by using Strapi types and TypeScript.
In this post, we will cover how to set Strapi types in your front end so you, too, can have these awesome benefits.
Let's get started.
Instead of starting from scratch, I have this simple starter project that we will use.
Go to the above repo and clone the project to your local computer. I will be using GitHub CLI since it is the easiest way.
In my terminal, I will run the following command.
gh repo clone PaulBratslavsky/strapi-next-js-no-types
Once you clone the project cd
into the project directory strapi-next-js-no-types
, run the following command to set up Strapi and Next JS.
yarn run setup
This will install all the required dependencies. Once it is done, we will seed our test data.
Before seeding the data, let's ensure you create a .env
file inside your backend
directory and paste the contents of the .env.example
file.
1 HOST=0.0.0.0
2 PORT=1337
3 APP_KEYS=AobODHcxZZcCl1QnSICbag==,ell4w82bPBXiCvJDn5ONHQ==,gWrzk+svSrpeSKrSwJt5Ng==,ifL/R/p7tizn8+6JZV99RA==
4 API_TOKEN_SALT=/PxsuKz4iVK8yiQx14lSNQ==
5 ADMIN_JWT_SECRET=9E3FdhrPvtS2fUtAV1sd9w==
6 TRANSFER_TOKEN_SALT=022gQbf/VKd6PyTWFp5oaw==
7 # Database
8 DATABASE_CLIENT=sqlite
9 DATABASE_FILENAME=.tmp/data.db
10 JWT_SECRET=J003pNoJHC0d4+EN6VPXwQ==
You can now run the following command to seed our test data.
yarn run seed
You will be prompted with the following message. Just click y
followed by the enter key to proceed.
? The import will delete your existing data! Are you sure you want to proceed?
(y/N)
After the process is complete, you should see the following.
Now that we have our basic data, we can start our application with the following command from the root of your project.
yarn run dev
You will be prompted to create your first Admin User. After creating the user and clicking Let's Start, you will be greeted by Strapi's Admin Panel.
You should find our test data and permissions to allow public api access set.
Let's take a look at at this basic code example. Currently we do not know what types data
returns from our API..
1export default async function Home() {
2 const data = await getData();
3
4 return (
5 <main className="flex min-h-screen flex-col items-center justify-between p-24">
6 <div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
7 {data.data.map((post: any, index: string) => {
8 return <Card key={index} data={post} />;
9 })}
10 </div>
11 </main>
12 );
13}
Let's add our Strapi types and fix this in the next section.
Prerequisites
To make this process easier, we will need these two things.
Strapi Content API for dynamic content API response types
Script to make copying our Strapi types to our project front end easy.
We will walk through the setup first, then we will go over the workflow.
Let's start by getting our types from Strapi.
All our generated Strapi types are in our backend/types/generated
folder in the file called contentTypes.d.ts.
We must have a copy of these types in our front-end project. To help us accomplish this, we will use the script provided above.
But first, run the following command to install @strapi/types to your project.
yarn add --dev @strapi/strapi
Make sure you run the command from the frontend
folder.
Inside the root of your project, create a file named copyTypes.js
and paste the following code.
1const fs = require("fs");
2const path = require("path");
3
4const destinationFolder = "frontend/src/types";
5
6const files = [
7 {
8 src: path.join(__dirname, "./backend/types/generated/contentTypes.d.ts"),
9 dest: path.join(__dirname, `./${destinationFolder}/contentTypes.d.ts`),
10 },
11 {
12 src: path.join(__dirname, "./backend/types/generated/components.d.ts"),
13 dest: path.join(__dirname, `./${destinationFolder}/components.d.ts`),
14 },
15];
16
17function copyFile({ src, dest }) {
18 const destinationDir = path.dirname(dest);
19
20 // Check if source file exists
21 if (!fs.existsSync(src)) {
22 console.error(`Source file does not exist: ${src}`);
23 process.exit(1);
24 }
25
26 // Ensure destination directory exists or create it
27 if (!fs.existsSync(destinationDir)) {
28 fs.mkdirSync(destinationDir, { recursive: true });
29 }
30
31 // Read the source file, modify its content and write to the destination file
32 const content = fs.readFileSync(src, "utf8");
33
34 fs.writeFile(dest, content, (err) => {
35 if (err) {
36 console.error(`Error writing to destination file: ${err}`);
37 process.exit(1);
38 } else {
39 console.log(`File ${src} copied and modified successfully!`);
40 }
41 });
42}
43
44files.forEach((file) => copyFile(file));
Let's update the package.json
file in the root with the following inside the scrips
object.
1 "copytypes": "node copyTypes.js"
You should now be able to run the following command.
yarn run copytypes
Go ahead and do so.
You should see the following message.
➜ strapi-next-js-no-types git:(strapi-typed) ✗ yarn run copytypes
File copied and modified successfully!
✨ Done in 0.22s.
And see a new types
folder found in frontend/src/types
with a new file called contentTypes.d.ts
.
Finally, create a new file inside frontend/src/types
folder named types.ts
and add the following code.
1import type { Attribute, Common, Utils } from "@strapi/strapi";
2
3type IDProperty = { id: number };
4
5type InvalidKeys<TSchemaUID extends Common.UID.Schema> = Utils.Object.KeysBy<
6 Attribute.GetAll<TSchemaUID>,
7 Attribute.Private | Attribute.Password
8>;
9
10export type GetValues<TSchemaUID extends Common.UID.Schema> = {
11 [TKey in Attribute.GetOptionalKeys<TSchemaUID>]?: Attribute.Get<
12 TSchemaUID,
13 TKey
14 > extends infer TAttribute extends Attribute.Attribute
15 ? GetValue<TAttribute>
16 : never;
17} & {
18 [TKey in Attribute.GetRequiredKeys<TSchemaUID>]-?: Attribute.Get<
19 TSchemaUID,
20 TKey
21 > extends infer TAttribute extends Attribute.Attribute
22 ? GetValue<TAttribute>
23 : never;
24} extends infer TValues
25 ? // Remove invalid keys (private, password)
26 Omit<TValues, InvalidKeys<TSchemaUID>>
27 : never;
28
29type RelationValue<TAttribute extends Attribute.Attribute> =
30 TAttribute extends Attribute.Relation<
31 infer _TOrigin,
32 infer TRelationKind,
33 infer TTarget
34 >
35 ? Utils.Expression.MatchFirst<
36 [
37 [
38 Utils.Expression.Extends<
39 TRelationKind,
40 Attribute.RelationKind.WithTarget
41 >,
42 TRelationKind extends `${string}ToMany`
43 ? Omit<APIResponseCollection<TTarget>, "meta">
44 : APIResponse<TTarget> | null
45 ]
46 ],
47 `TODO: handle other relation kind (${TRelationKind})`
48 >
49 : never;
50
51type ComponentValue<TAttribute extends Attribute.Attribute> =
52 TAttribute extends Attribute.Component<infer TComponentUID, infer TRepeatable>
53 ? IDProperty &
54 Utils.Expression.If<
55 TRepeatable,
56 GetValues<TComponentUID>[],
57 GetValues<TComponentUID> | null
58 >
59 : never;
60
61type DynamicZoneValue<TAttribute extends Attribute.Attribute> =
62 TAttribute extends Attribute.DynamicZone<infer TComponentUIDs>
63 ? Array<
64 Utils.Array.Values<TComponentUIDs> extends infer TComponentUID
65 ? TComponentUID extends Common.UID.Component
66 ? { __component: TComponentUID } & IDProperty &
67 GetValues<TComponentUID>
68 : never
69 : never
70 >
71 : never;
72
73type MediaValue<TAttribute extends Attribute.Attribute> =
74 TAttribute extends Attribute.Media<infer _TKind, infer TMultiple>
75 ? Utils.Expression.If<
76 TMultiple,
77 APIResponseCollection<"plugin::upload.file">,
78 APIResponse<"plugin::upload.file"> | null
79 >
80 : never;
81
82export type GetValue<TAttribute extends Attribute.Attribute> =
83 Utils.Expression.If<
84 Utils.Expression.IsNotNever<TAttribute>,
85 Utils.Expression.MatchFirst<
86 [
87 // Relation
88 [
89 Utils.Expression.Extends<TAttribute, Attribute.OfType<"relation">>,
90 RelationValue<TAttribute>
91 ],
92 // DynamicZone
93 [
94 Utils.Expression.Extends<TAttribute, Attribute.OfType<"dynamiczone">>,
95 DynamicZoneValue<TAttribute>
96 ],
97 // Component
98 [
99 Utils.Expression.Extends<TAttribute, Attribute.OfType<"component">>,
100 ComponentValue<TAttribute>
101 ],
102 // Media
103 [
104 Utils.Expression.Extends<TAttribute, Attribute.OfType<"media">>,
105 MediaValue<TAttribute>
106 ],
107 // Fallback
108 // If none of the above attribute type, fallback to the original Attribute.GetValue (while making sure it's an attribute)
109 [Utils.Expression.True, Attribute.GetValue<TAttribute, unknown>]
110 ],
111 unknown
112 >,
113 unknown
114 >;
115
116export interface APIResponseData<TContentTypeUID extends Common.UID.ContentType>
117 extends IDProperty {
118 attributes: GetValues<TContentTypeUID>;
119}
120
121export interface APIResponseCollectionMetadata {
122 pagination: {
123 page: number;
124 pageSize: number;
125 pageCount: number;
126 total: number;
127 };
128}
129
130export interface APIResponse<TContentTypeUID extends Common.UID.ContentType> {
131 data: APIResponseData<TContentTypeUID>;
132}
133
134export interface APIResponseCollection<
135 TContentTypeUID extends Common.UID.ContentType
136> {
137 data: APIResponseData<TContentTypeUID>[];
138 meta: APIResponseCollectionMetadata;
139}
This file includes helpful interfaces to help us utilize our Strapi types.
In the next section we will put this to use.
Now that our project is set up, let's use our types.
Inside your frontend project, navigate to frontend/srs/app/page.tsx
and replace the code with the following update.
1// import our types
2import type { APIResponseCollection, APIResponseData } from "@/types/strapi";
3
4import qs from "qs";
5const query = qs.stringify({ populate: "*" });
6
7async function getData() {
8 const res = await fetch("http://127.0.0.1:1337/api/posts?" + query);
9
10 if (!res.ok) {
11 // This will activate the closest `error.js` Error Boundary
12 throw new Error("Failed to fetch data");
13 }
14
15 return res.json();
16}
17
18export default async function Home() {
19 // typescript will infer the type of `data` as `APIResponseCollection<"api::post.post">`
20 const data = (await getData()) as APIResponseCollection<"api::post.post">;
21
22 return (
23 <main className="flex min-h-screen flex-col items-center justify-between p-24">
24 <div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
25 {data.data.map((post: APIResponseData<"api::post.post">) => {
26 return <Card key={post.id} data={post} />;
27 })}
28 </div>
29 </main>
30 );
31}
32
33// typescript will infer the type of `data` as `APIResponseData<"api::post.post">`
34// Look up types by content id "api::post.post"
35function Card({ data }: { data: APIResponseData<"api::post.post"> }) {
36 console.log(data, "############# STRAPI CARD DATA #############");
37
38 const { title, description } = data.attributes;
39 return (
40 <a
41 href="https://strapi.io"
42 className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
43 target="_blank"
44 rel="noopener noreferrer"
45 >
46 <h2 className={`mb-3 text-2xl font-semibold`}>
47 {title}{" "}
48 <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
49 ->
50 </span>
51 </h2>
52 <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>{description}</p>
53 </a>
54 );
55}
Notice in the gif below that we are able to infer our Strapi types.
Before finishing up, let's add another field to our post collection and walk through the workflow.
Inside Strapi Content Builder let's add a new field. We will add an Enumeration
field named status
with the following options.
copytypes
ScriptRun the following command to update Strapi types in the front end.
yarn run copytypes
Restart your application. And you will now see our status
field.
With these changes, TypeScript will now help us ensure we are using the correct data types, making our application more reliable and reducing potential runtime errors.
This tutorial delved into improving the frontend experience by utilizing Strapi types and TypeScript together.
By following the steps outlined, the real magic unveils itself.
When applying these predefined Strapi types within our frontend project.
It leads to clearer, more maintainable code. It helps prevent many common mistakes when developers have to guess the shape or type of data they are working with.
This is just the start, and while we wait for the SDK, I hope you find this approach helpful.
Feel free to share your tips for using TS in the comment below.
GitHub: Starter Code GitHub: Final Code