# Cloud account billing details Source: https://docs.strapi.io/cloud/account/account-billing # Cloud account billing & invoices Billing details and invoices are managed on the Profile page, where payment methods are updated and invoice history is available. Through the *Profile* page, accessible by clicking on your profile picture on the top right hand corner of the interface then clicking on **Profile**, you can access the [ *Billing*](#account-billing) and [ *Invoices*](#account-invoices) tabs. ## Account billing The *Billing* tab displays and enables you to modify the billing details and payment method set for the account. The *Payment method* section of the *Billing* tab allows you to manage the credit cards that can be used for the Strapi Cloud projects. The *Billing details* section requires to be filled in, at least for the mandatory fields, as this information will be the default billing details for all Strapi Cloud projects related to your account. ### Adding a new credit card 1. In the *Payment method* section of the *Billing* tab, click on the **Add card** button. 2. Fill in the following fields: | Field name | Description | | --- | --- | | Card Number | Write the number of the credit card to add as payment method. | | Expires | Write the expiration date of the credit card. | | CVC | Write the 3-numbers code displayed at the back of the credit card. | 3. Click on the **Save** button. :::tip The first credit card to be added as payment method for the account will by default be the primary one. It is however possible to define another credit card as primary by clicking on the icon, then **Switch as primary**. ::: ### Deleting a credit card To remove a credit card from the list of payment methods for the account: 1. Click on the icon of the credit card you wish to delete. 2. Click **Remove card**. The card is immediately deleted. :::note You cannot delete the primary card as at least one credit card must be available as payment method, and the primary card is by default that one. If the credit card you wish to delete is currently the primary card, you must first define another credit card as primary, then delete it. ::: ## Account invoices The *Invoices* tab displays the complete list of invoices for all your Strapi Cloud projects. Invoices can have any of the following statuses: - Paid: the payment has been done and the invoice is available, no additional action is required. - Payment pending: the invoice is not complete or validated yet - Payment due: the payment didn't go through and needs to be fixed - Not paid: the payment has failed and won't automatically be retried - Voided: the invoice has been cancelled. :::tip Click the ![download icon](/img/assets/icons/download.svg) icon to download an invoice. ::: :::strapi Invoices are also available per project. In the *Settings > Invoices* tab of any project, you will find the invoices for that project only. Feel free to check the [dedicated documentation](/cloud/projects/settings#invoices). ::: # Cloud profile settings Source: https://docs.strapi.io/cloud/account/account-settings # Cloud profile settings Settings of the Profile page include account details, connected accounts, and account deletion options. The *Profile* page enables you to manage your account details and preferences. It is accessible by clicking on your profile picture, on the top right hand corner of the interface, and **Profile**. There are 3 tabs available in the *Profile* interface: [*General*](#general), *Billing* and Invoices (the last 2 are documented in the [Account billing details](/cloud/account/account-billing) section of this documentation). ## General The *General* tab enables you to edit the following details for your account profile: - Details: to see the name associated with your account. - Connected accounts: to manage Google, GitHub, GitLab and email accounts connected with your Strapi Cloud account (see [Managing connected accounts](#managing-connected-accounts)). - Delete account: to permanently delete your Strapi Cloud account (see [Deleting Strapi Cloud account](#deleting-strapi-cloud-account)). ### Managing connected accounts You can connect a Google, GitLab, GitHub and email account to your Strapi Cloud account. The _Connected accounts_ section lists accounts that are currently connected to your Strapi Cloud account. From there you can also connect a new Google, GitLab, GitHub and email account if one is not already connected. To connect a new Google, GitLab, GitHub or email account to your Strapi Cloud account, click on the **Connect account** button and follow the next steps on the corresponding website. You can also click on the three dots button of a connected account and click on the "Manage on" button to manage your GitHub, GitLab or Google account directly on the corresponding website. ### Deleting Strapi Cloud account You can delete your Strapi Cloud account, but it will be permanent and irreversible. All associated projects and their data will be deleted as well and the subscriptions for the projects will automatically be canceled. 1. In the *Delete account* section of the *General* tab, click on the **Delete account** button. 2. In the dialog, type `DELETE` in the textbox. 3. Confirm the deletion of your account by clicking on the **Delete** button. # Cloud database configuration Source: https://docs.strapi.io/cloud/advanced/database # Cloud database configuration Default PostgreSQL can be swapped for any supported SQL database by aligning configuration and environment variables. Strapi Cloud provides a pre-configured PostgreSQL database by default. However, you can also configure it to utilize an external SQL database, if needed. :::prerequisites - A local Strapi project running on `v4.8.2+`. - Credentials for an external database. - If using an existing database, the schema must match the Strapi project schema. ::: :::caution While it's possible to use an external database with Strapi Cloud, you should do it while keeping in mind the following considerations: - Strapi Cloud already provides a managed database that is optimized for Strapi. - Using an external database may result in unexpected behavior and/or performance issues (e.g., network latency may impact performance). For performance reasons, it's recommended to host your external database close to the region where your Strapi Cloud project is hosted. You can find where your Strapi Cloud project is hosted in your Project Settings (see [Project Settings > General > Selected Region](/cloud/projects/settings#general)). - Strapi can't provide security or support with external databases used with Strapi Cloud. ::: :::warning Any environment variable added to your project that starts with `DATABASE_` will cause Strapi Cloud to assume that you will be using an external database and all Strapi Cloud specific database variables will not be injected! ::: ## Configuration The project `/config/database.js` or `/config/database.ts` file must match the configuration found in the [environment variables in database configurations](https://docs.strapi.io/cms/configurations/database#environment-variables-in-database-configurations) section. Before pushing changes, add environment variables to the Strapi Cloud project: 1. Log into Strapi Cloud and click on the corresponding project on the Projects page. 2. Click on the **Settings** tab and choose **Variables** in the left menu. 3. Add the following environment variables: | Variable | Value | Details | | ---------------------------------- | ---------------- |----------| | `DATABASE_CLIENT` | your_db | Should be one of `mysql`, `postgres`, or `sqlite`. | | `DATABASE_HOST` | your_db_host | The URL or IP address of your database host | | `DATABASE_PORT` | your_db_port | The port to access your database | | `DATABASE_NAME` | your_db_name | The name of your database | | `DATABASE_USERNAME` | your_db_username | The username to access your database | | `DATABASE_PASSWORD` | your_db_password | The password associated to this username | | `DATABASE_SSL_REJECT_UNAUTHORIZED` | false | Whether unauthorized connections should be rejected | | `DATABASE_SCHEMA` | public | - | 4. Click **Save**. :::caution To ensure a smooth deployment, it is recommended to not change the names of the environment variables. ::: ## Deployment To deploy the project and utilize the external database, push the changes from earlier. This will trigger a rebuild and new deployment of the Strapi Cloud project. Once the application finishes building, the project will use the external database. ## Reverting to the default database To revert back to the default database, remove the previously added environment variables related to the external database from the Strapi Cloud project dashboard, and save. For the changes to take effect, you must redeploy the Strapi Cloud project. # Cloud email provider Source: https://docs.strapi.io/cloud/advanced/email # Email Providers configuration for Strapi Cloud Third‑party email services integrate through plugins and environment variables to replace the default sender. Strapi Cloud comes with a basic email provider out of the box. However, it can also be configured to utilize another email provider, if needed. :::caution Please be advised that Strapi is unable to provide support for third-party email providers. ::: :::prerequisites - A local Strapi project running on `v4.8.2+`. - Credentials for another email provider (see [Strapi Market](https://market.strapi.io/providers)). ::: ## Configuration Configuring another email provider for use with Strapi Cloud requires 3 steps: 1. Install the provider plugin in your local Strapi project. 2. Configure the provider in your local Strapi project. 3. Add environment variables to the Strapi Cloud project. ### Install the Provider Plugin Using either `npm` or `yarn`, install the provider plugin in your local Strapi project as a package dependency by following the instructions in the respective entry for that provider in the [Marketplace](https://market.strapi.io/providers). ### Configure the Provider In your Strapi project, create a `/config/env/production/plugins.js` or `/config/env/production/plugins.ts` file with the following content: ```js title=/config/env/production/plugins.js module.exports = ({ env }) => ({ // … some unrelated plugins configuration options // highlight-start email: { config: { // … provider-specific upload configuration options go here } // highlight-end // … some other unrelated plugins configuration options } }); ``` ```ts title=/config/env/production/plugins.ts // … some unrelated plugins configuration options // highlight-start email: { config: { // … provider-specific upload configuration options go here } // highlight-end // … some other unrelated plugins configuration options } }); ``` :::caution The file structure must match the above path exactly, or the configuration will not be applied to Strapi Cloud. ::: Each provider will have different configuration settings available. Review the respective entry for that provider in the [Marketplace](https://market.strapi.io/providers). **Example:** ```js title=/config/env/production/plugins.js module.exports = ({ env }) => ({ // ... email: { config: { provider: 'sendgrid', providerOptions: { apiKey: env('SENDGRID_API_KEY'), }, settings: { defaultFrom: 'myemail@protonmail.com', defaultReplyTo: 'myemail@protonmail.com', }, }, }, // ... }); ``` ```js title=/config/env/production/plugins.js module.exports = ({ env }) => ({ // ... email: { config: { provider: 'amazon-ses', providerOptions: { key: env('AWS_SES_KEY'), secret: env('AWS_SES_SECRET'), amazon: 'https://email.us-east-1.amazonaws.com', }, settings: { defaultFrom: 'myemail@protonmail.com', defaultReplyTo: 'myemail@protonmail.com', }, }, }, // ... }); ``` ```js title=/config/env/production/plugins.js module.exports = ({ env }) => ({ // ... email: { config: { provider: 'mailgun', providerOptions: { key: env('MAILGUN_API_KEY'), // Required domain: env('MAILGUN_DOMAIN'), // Required url: env('MAILGUN_URL', 'https://api.mailgun.net'), //Optional. If domain region is Europe use 'https://api.eu.mailgun.net' }, settings: { defaultFrom: 'myemail@protonmail.com', defaultReplyTo: 'myemail@protonmail.com', }, }, }, // ... }); ``` ```ts title=/config/env/production/plugins.ts // ... email: { config: { provider: 'sendgrid', providerOptions: { apiKey: env('SENDGRID_API_KEY'), }, settings: { defaultFrom: 'myemail@protonmail.com', defaultReplyTo: 'myemail@protonmail.com', }, }, }, // ... }); ``` ```ts title=/config/env/production/plugins.ts // ... email: { config: { provider: 'amazon-ses', providerOptions: { key: env('AWS_SES_KEY'), secret: env('AWS_SES_SECRET'), amazon: 'https://email.us-east-1.amazonaws.com', }, settings: { defaultFrom: 'myemail@protonmail.com', defaultReplyTo: 'myemail@protonmail.com', }, }, }, // ... }); ``` ```ts title=/config/env/production/plugins.ts // ... email: { config: { provider: 'mailgun', providerOptions: { key: env('MAILGUN_API_KEY'), // Required domain: env('MAILGUN_DOMAIN'), // Required url: env('MAILGUN_URL', 'https://api.mailgun.net'), //Optional. If domain region is Europe use 'https://api.eu.mailgun.net' }, settings: { defaultFrom: 'myemail@protonmail.com', defaultReplyTo: 'myemail@protonmail.com', }, }, }, // ... }); ``` :::tip Before pushing the above changes to GitHub, add environment variables to the Strapi Cloud project to prevent triggering a rebuild and new deployment of the project before the changes are complete. ::: ### Strapi Cloud Configuration 1. Log into Strapi Cloud and click on the corresponding project on the Projects page. 2. Click on the **Settings** tab and choose **Variables** in the left menu. 3. Add the required environment variables specific to the email provider. 4. Click **Save**. **Example:** | Variable | Value | |--------------------|-----------------------| | `SENDGRID_API_KEY` | your_sendgrid_api_key | | Variable | Value | |------------------|---------------------| | `AWS_SES_KEY` | your_aws_ses_key | | `AWS_SES_SECRET` | your_aws_ses_secret | | Variable | Value | |-------------------|----------------------| | `MAILGUN_API_KEY` | your_mailgun_api_key | | `MAILGUN_DOMAIN` | your_mailgun_domain | | `MAILGUN_URL` | your_mailgun_url | ## Deployment To deploy the project and utilize another party email provider, push the changes from earlier. This will trigger a rebuild and new deployment of the Strapi Cloud project. Once the application finishes building, the project will use the new email provider. :::strapi Custom Provider If you want to create a custom email provider, please refer to the [Email providers](/cms/features/email#providers) documentation in the CMS Documentation. ::: # Middleware Configuration for Strapi Cloud Source: https://docs.strapi.io/cloud/advanced/middlewares # Middleware Configuration for Strapi Cloud On Strapi Cloud, middleware customizations must go in `config/env/production/middlewares`. Changes to the global config file are overwritten on deploy. :::prerequisites - A local Strapi project. - A Strapi Cloud project (see [Getting Started](/cloud/getting-started/deployment)). ::: On Strapi Cloud, `NODE_ENV` is always set to `production`. The platform applies its own production-level middleware configuration on deploy. Any changes to the global `config/middlewares` file are overwritten and will not take effect. For available middleware options, see [Middlewares configuration](/cms/configurations/middlewares). To apply custom middleware configuration on Strapi Cloud, place your changes in: ``` config/env/production/middlewares.js ``` ``` config/env/production/middlewares.ts ``` :::caution The `config/env/production/middlewares` file **fully replaces** the global middleware array. Your file must include the complete list: - `strapi::errors` - `strapi::security` - `strapi::cors` - `strapi::poweredBy` - `strapi::logger` - `strapi::query` - `strapi::body` - `strapi::session` - `strapi::favicon` - `strapi::public` Both CSP and CORS customizations can be combined in the same file. ::: :::note - You can keep your existing `config/middlewares` file as-is as it will not cause conflicts. The production-specific file takes precedence on Strapi Cloud. - Upload size limits on Strapi Cloud are enforced at the infrastructure level and cannot be overridden via the `strapi::body` config. - For per-plan values and the memory-based recommendation for image uploads, see [Upload size limits for Strapi Cloud](/cloud/advanced/upload-size-limits). For external storage options, see [Upload Provider Configuration](/cloud/advanced/upload). ::: ## Custom Content Security Policy (CSP) If you use an external upload provider, allow its domain in the CSP directives. Without this, the Strapi Admin panel will block images and media from those sources. Create or update `config/env/production/middlewares`: ```js title="config/env/production/middlewares.js" module.exports = [ 'strapi::errors', { name: 'strapi::security', config: { contentSecurityPolicy: { useDefaults: true, directives: { 'connect-src': ["'self'", 'https:'], 'img-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'your-custom-domain.com', // replace with your provider domain ], 'media-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'your-custom-domain.com', // replace with your provider domain ], upgradeInsecureRequests: null, }, }, }, }, 'strapi::cors', 'strapi::poweredBy', 'strapi::logger', 'strapi::query', 'strapi::body', 'strapi::session', 'strapi::favicon', 'strapi::public', ]; ``` ```ts title="config/env/production/middlewares.ts" 'strapi::errors', { name: 'strapi::security', config: { contentSecurityPolicy: { useDefaults: true, directives: { 'connect-src': ["'self'", 'https:'], 'img-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'your-custom-domain.com', // replace with your provider domain ], 'media-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'your-custom-domain.com', // replace with your provider domain ], upgradeInsecureRequests: null, }, }, }, }, 'strapi::cors', 'strapi::poweredBy', 'strapi::logger', 'strapi::query', 'strapi::body', 'strapi::session', 'strapi::favicon', 'strapi::public', ]; ``` :::tip For a full list of upload providers and their required domains, see the [Strapi Market](https://market.strapi.io/providers). ::: ## Custom CORS headers If your frontend sends custom request headers (e.g. for authorization flows), you need to explicitly allow them in the CORS configuration. Placing this in the global `config/middlewares` file will not work on Strapi Cloud. Place it in `config/env/production/middlewares` instead. ```js title="config/env/production/middlewares.js" module.exports = ({ env }) => [ 'strapi::errors', 'strapi::security', { name: 'strapi::cors', config: { enabled: true, origin: [env('CLIENT_URL')], headers: [ 'Content-Type', 'Authorization', 'Origin', 'Accept', 'X-Requested-With', 'your-custom-header', // add any custom headers your frontend sends ], }, }, 'strapi::poweredBy', 'strapi::logger', 'strapi::query', 'strapi::body', 'strapi::session', 'strapi::favicon', 'strapi::public', ]; ``` ```ts title="config/env/production/middlewares.ts" 'strapi::errors', 'strapi::security', { name: 'strapi::cors', config: { enabled: true, origin: [env('CLIENT_URL')], headers: [ 'Content-Type', 'Authorization', 'Origin', 'Accept', 'X-Requested-With', 'your-custom-header', // add any custom headers your frontend sends ], }, }, 'strapi::poweredBy', 'strapi::logger', 'strapi::query', 'strapi::body', 'strapi::session', 'strapi::favicon', 'strapi::public', ]; ``` # Upload Provider Configuration for Strapi Cloud Source: https://docs.strapi.io/cloud/advanced/upload # Upload Provider Configuration for Strapi Cloud External storage like S3 or Cloudinary requires plugin setup, security middleware, and Cloud variables. Strapi Cloud comes with a local upload provider out of the box. However, it can also be configured to use a third-party upload provider, if needed. :::note For the file size and memory-based limits that apply to uploads, see [Upload size limits for Strapi Cloud](/cloud/advanced/upload-size-limits). ::: ## Configuring a third-party upload provider :::caution Please be advised that Strapi is unable to provide support for third-party upload providers. ::: :::prerequisites - A local Strapi project running on `v4.8.2+`. - Credentials for a third-party upload provider (see [Strapi Market](https://market.strapi.io/providers)). ::: Configuring a third-party upload provider for use with Strapi Cloud requires the following 4 configuration steps, followed by a deployment: 1. Install the provider plugin in your local Strapi project. 2. Configure the provider in your local Strapi project. 3. Configure the security middleware in your local Strapi project. 4. Add environment variables to the Strapi Cloud project. ### Install the provider plugin Using either `npm` or `yarn`, install the provider plugin in your local Strapi project as a package dependency by following the instructions in the respective entry for that provider in the [Marketplace](https://market.strapi.io/providers). ### Configure the provider To configure a third-party upload provider in your Strapi project, create or edit the plugins configuration file for your production environment `/config/env/production/plugins.js|ts` by adding upload configuration options as follows: ```js title=/config/env/production/plugins.js module.exports = ({ env }) => ({ // … some unrelated plugins configuration options // highlight-start upload: { config: { // … provider-specific upload configuration options go here } // highlight-end // … some other unrelated plugins configuration options } }); ``` ```ts title=/config/env/production/plugins.ts // … some unrelated plugins configuration options // highlight-start upload: { config: { // … provider-specific upload configuration options go here } // highlight-end // … some other unrelated plugins configuration options } }); ``` :::caution The file structure must match the above path exactly, or the configuration will not be applied to Strapi Cloud. ::: Each provider will have different configuration settings available. Review the respective entry for that provider in the [Marketplace](https://market.strapi.io/providers). **Example:** ```js title=/config/env/production/plugins.js module.exports = ({ env }) => ({ // ... upload: { config: { provider: 'cloudinary', providerOptions: { cloud_name: env('CLOUDINARY_NAME'), api_key: env('CLOUDINARY_KEY'), api_secret: env('CLOUDINARY_SECRET'), }, actionOptions: { upload: {}, uploadStream: {}, delete: {}, }, }, }, // ... }); ``` :::tip For full S3 provider configuration details (credential formats, extended options, S3-compatible services), see the [Amazon S3 provider](/cms/configurations/media-library-providers/amazon-s3) page in the CMS documentation. ::: ```js title=/config/env/production/plugins.js module.exports = ({ env }) => ({ // ... upload: { config: { provider: 'aws-s3', providerOptions: { baseUrl: env('CDN_URL'), rootPath: env('CDN_ROOT_PATH'), s3Options: { credentials: { accessKeyId: env('AWS_ACCESS_KEY_ID'), secretAccessKey: env('AWS_ACCESS_SECRET'), }, region: env('AWS_REGION'), params: { ACL: env('AWS_ACL', 'public-read'), signedUrlExpires: env('AWS_SIGNED_URL_EXPIRES', 15 * 60), Bucket: env('AWS_BUCKET'), }, }, }, actionOptions: { upload: {}, uploadStream: {}, delete: {}, }, }, }, // ... }); ``` ```ts title=/config/env/production/plugins.ts // ... upload: { config: { provider: 'cloudinary', providerOptions: { cloud_name: env('CLOUDINARY_NAME'), api_key: env('CLOUDINARY_KEY'), api_secret: env('CLOUDINARY_SECRET'), }, actionOptions: { upload: {}, uploadStream: {}, delete: {}, }, }, }, // ... }); ``` ```ts title=/config/env/production/plugins.ts // ... upload: { config: { provider: 'aws-s3', providerOptions: { baseUrl: env('CDN_URL'), rootPath: env('CDN_ROOT_PATH'), s3Options: { credentials: { accessKeyId: env('AWS_ACCESS_KEY_ID'), secretAccessKey: env('AWS_ACCESS_SECRET'), }, region: env('AWS_REGION'), params: { ACL: env('AWS_ACL', 'public-read'), signedUrlExpires: env('AWS_SIGNED_URL_EXPIRES', 15 * 60), Bucket: env('AWS_BUCKET'), }, }, }, actionOptions: { upload: {}, uploadStream: {}, delete: {}, }, }, }, // ... }); ``` ### Configure the security middleware Due to the default settings in the Strapi security middleware you will need to modify the `contentSecurityPolicy` settings to properly see thumbnail previews in the Media Library. :::caution On Strapi Cloud, `NODE_ENV` is always set to `production`. Changes to the global `config/middlewares.ts` file are overwritten on each deploy and will not take effect. Place your Security Middleware customizations in `config/env/production/middlewares.ts` instead. See [Middleware Configuration for Strapi Cloud](/cloud/advanced/middlewares) for details. ::: To do this in your Strapi project: 1. Navigate to `/config/env/production/middlewares.js` or `/config/env/production/middlewares.ts` in your Strapi project. 2. Replace the default `strapi::security` string with the object provided by the upload provider. **Example:** ```js title=/config/env/production/middlewares.js module.exports = [ // ... { name: 'strapi::security', config: { contentSecurityPolicy: { useDefaults: true, directives: { 'connect-src': ["'self'", 'https:'], 'img-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'res.cloudinary.com' ], 'media-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'res.cloudinary.com', ], upgradeInsecureRequests: null, }, }, }, }, // ... ]; ``` ```js title=/config/env/production/middlewares.js module.exports = [ // ... { name: 'strapi::security', config: { contentSecurityPolicy: { useDefaults: true, directives: { 'connect-src': ["'self'", 'https:'], 'img-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'yourBucketName.s3.yourRegion.amazonaws.com', ], 'media-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'yourBucketName.s3.yourRegion.amazonaws.com', ], upgradeInsecureRequests: null, }, }, }, }, // ... ]; ``` ```ts title=/config/env/production/middlewares.ts // ... { name: 'strapi::security', config: { contentSecurityPolicy: { useDefaults: true, directives: { 'connect-src': ["'self'", 'https:'], 'img-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'res.cloudinary.com' ], 'media-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'res.cloudinary.com', ], upgradeInsecureRequests: null, }, }, }, }, // ... ]; ``` ```ts title=/config/env/production/middlewares.ts // ... { name: 'strapi::security', config: { contentSecurityPolicy: { useDefaults: true, directives: { 'connect-src': ["'self'", 'https:'], 'img-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'yourBucketName.s3.yourRegion.amazonaws.com', ], 'media-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'yourBucketName.s3.yourRegion.amazonaws.com', ], upgradeInsecureRequests: null, }, }, }, }, // ... ]; ``` :::tip Before pushing the above changes to GitHub, add environment variables to the Strapi Cloud project to prevent triggering a rebuild and new deployment of the project before the changes are complete. ::: ### Strapi Cloud configuration 1. Log into Strapi Cloud and click on the corresponding project on the Projects page. 2. Click on the **Settings** tab and choose **Variables** in the left menu. 3. Add the required environment variables specific to the upload provider. 4. Click **Save**. **Example:** | Variable | Value | |---------------------|-------------------------| | `CLOUDINARY_NAME` | your_cloudinary_name | | `CLOUDINARY_KEY` | your_cloudinary_api_key | | `CLOUDINARY_SECRET` | your_cloudinary_secret | | Variable | Value | |---------------------|------------------------| | `AWS_ACCESS_KEY_ID` | your_aws_access_key_id | | `AWS_ACCESS_SECRET` | your_aws_access_secret | | `AWS_REGION` | your_aws_region | | `AWS_BUCKET` | your_aws_bucket | | `CDN_URL` | your_cdn_url | | `CDN_ROOT_PATH` | your_cdn_root_path | ### Deployment To deploy the project and use the third-party upload provider, push the changes from earlier. This will trigger a rebuild and new deployment of the Strapi Cloud project. Once the application finishes building, the project will use the new upload provider. :::strapi Custom Provider If you want to create a custom upload provider, please refer to the [Providers](/cms/features/media-library#providers) documentation in the CMS Documentation. ::: # Upload size limits for Strapi Cloud Source: https://docs.strapi.io/cloud/advanced/upload-size-limits # Upload size limits for Strapi Cloud Non-image files are capped at 200 MB on all plans. Image files have a memory-based recommended maximum that varies by format, plan, and Media Library settings. To upload larger images, disable Responsive friendly upload and Size optimization. Strapi Cloud applies 2 distinct limits to uploads. The first is a hard maximum file size, enforced at the infrastructure level for non-image files. The second is a memory-based recommendation for image files that depends on your CMS settings. ## Maximum upload file size for non-image files Non-image uploads are capped at 200 MB on all Strapi Cloud plans (Free, Essential, Pro, and Scale). The cap is enforced at the infrastructure level and cannot be overridden via the `strapi::body` middleware configuration. ## Recommended maximum upload size for image files Image uploads are subject to an additional, memory-driven recommendation that is independent of the non-image cap and varies by image format. :::tip Uploading a large image? To upload an image larger than the recommended maximum, disable both Responsive friendly upload and Size optimization in the [Media Library settings](/cms/features/media-library#configuring-settings). The CMS then stores the source file as-is without in-process processing, which raises the recommended maximum (see the _Processing off_ values in the tables below). ::: When Responsive friendly upload and Size optimization are both enabled in the [Media Library settings](/cms/features/media-library#configuring-settings), the CMS resizes the source image and generates a set of thumbnails (small, medium, large) in the instance's memory before persisting them. This processing happens in-process, regardless of the configured upload provider. Switching to a third-party provider (Amazon S3, Cloudinary, etc.) is not a workaround. The resize and thumbnail generation step still runs inside the Strapi Cloud instance and still requires memory proportional to the source image dimensions. Actual memory usage depends on the image dimensions, format, and your Media Library settings. The values in the following tables are a recommendation, not a hard limit. Uploads above the recommended size are likely to cause the instance to run out of memory and restart. The recommendation depends on whether Responsive friendly upload and Size optimization are enabled in the [Media Library settings](/cms/features/media-library#configuring-settings): - _Processing on_: both settings enabled, with the default `small`, `medium`, and `large` sizes. Strapi generates the thumbnails in memory, so the safe upload size is lower. - _Processing off_: both settings disabled. The source image is stored as-is with no in-process processing, so the safe upload size is higher. Recommended maximum image size, expressed in megapixels (MP), per format and plan: | Format | Free & Essential | Pro & Scale | |--------|------------------|-------------| | JPEG | 24 MP | 135 MP | | PNG | 8 MP | 90 MP | | WebP | 4 MP | 12 MP | | TIFF | 16 MP | 125 MP | | AVIF | 66 MP | 80 MP | | Format | Free & Essential | Pro & Scale | |--------|------------------|-------------| | JPEG | 200 MP | 265 MP | | PNG | 20 MP | 115 MP | | WebP | 15 MP | 40 MP | | TIFF | 20 MP | 125 MP | | AVIF | 74 MP | 90 MP | :::note Converting pixels to megapixels The number of megapixels of an image is its width multiplied by its height in pixels, divided by 1,000,000. The pixel dimensions that match a given megapixel count depend on the aspect ratio. For a square image, 1 MP is roughly 1000×1000 px, 4 MP is roughly 2000×2000 px, and 100 MP is roughly 10000×10000 px. ::: :::strapi Configuring a provider To configure external storage such as Amazon S3 or Cloudinary, see [Upload Provider Configuration for Strapi Cloud](/cloud/advanced/upload). ::: # Command Line Interface (CLI) Source: https://docs.strapi.io/cloud/cli/cloud-cli # Command Line Interface (CLI) CLI commands handle login, project deploy, linking, listing, and logout without needing a remote repository. Strapi Cloud comes with a Command Line Interface (CLI) which allows you to log in and out, and to deploy a local project without it having to be hosted on a remote git repository. The CLI works with both the `yarn` and `npm` package managers. :::note It is recommended to install Strapi locally only, which requires prefixing all of the following `strapi` commands with the package manager used for the project setup (e.g `npm run strapi help` or `yarn strapi help`) or a dedicated node package executor (e.g. `npx strapi help`). ::: ## strapi login **Alias:** `strapi cloud:login` Log in Strapi Cloud. ```bash strapi login ``` This command automatically opens a browser window to first ask you to confirm that the codes displayed in both the browser window and the terminal are the same. Then you will be able to log into Strapi Cloud via Google, GitHub or GitLab. Once the browser window confirms successful login, it can be safely closed. If the browser window doesn't automatically open, the terminal will display a clickable link as well as the code to enter manually. ## strapi deploy **Alias:** `strapi cloud:deploy` Deploy a new local project (< 100MB) in Strapi Cloud. ```bash strapi deploy ``` This command must be used after the `login` one. It deploys a local Strapi project on Strapi Cloud, without having to host it on a remote git repository beforehand. The terminal will inform you when the project is successfully deployed on Strapi Cloud. Deploying a Strapi project through the CLI creates a project on the Free plan. Once the project is first deployed on Strapi Cloud with the CLI, the `deploy` command can be reused to trigger a new deployment of the same project. :::note Once you deployed your project, if you visit the Strapi Cloud dashboard, you may see some limitations as well as impacts due to creating a Strapi Cloud project that is not in a remote repository and which was deployed with the CLI. - Some areas in the dashboard that are usually reserved to display information about the git provider will be blank. - Some buttons, such as the **Trigger deploy** button, will be greyed out and unclickable since, unless you have [connected a git repository to your Strapi Cloud project](/cloud/getting-started/deployment-cli#automatically-deploying-subsequent-changes). ::: ## strapi link **Alias:** `strapi cloud:link` Links project in the current folder to an existing project in Strapi Cloud. ```bash strapi link ``` This command connects your local project in the current directory with an existing project on your Strapi Cloud account. You will be prompted to select the project you wish to link from a list of available projects hosted on Strapi Cloud. ## strapi projects **Alias:** `strapi cloud:projects` Lists all Strapi Cloud projects associated with your account. ```bash strapi projects ``` This command retrieves and displays a list of all projects hosted on your Strapi Cloud account. ## strapi logout **Alias:** `strapi cloud:logout` Log out of Strapi Cloud. ```bash strapi logout ``` This command logs you out of Strapi Cloud. Once the `logout` command is run, a browser page will open and the terminal will display a confirmation message that you were successfully logged out. You will not be able to use the `deploy` command anymore. # Strapi Cloud fundamentals Source: https://docs.strapi.io/cloud/cloud-fundamentals # Strapi Cloud fundamentals Strapi Cloud is a PaaS hosting platform for deploying Strapi CMS projects, offering four pricing plans with different features and support levels, two user roles (owners and maintainers), and REST/GraphQL APIs that behave identically to self-hosted servers. Before going any further into this Strapi Cloud documentation, we recommend you to acknowledge the main concepts below. They will help you to understand how Strapi Cloud works, and ensure a smooth Strapi Cloud experience. - **Hosting Platform**
Strapi Cloud is a hosting platform that allows to deploy already existing Strapi projects created with Strapi CMS (Content Management System). Strapi Cloud is *not* the SaaS ([Software-as-a-Service](https://en.wikipedia.org/wiki/Software_as_a_service)) version of Strapi CMS and should rather be considered as a PaaS ([Platform-as-a-Service](https://en.wikipedia.org/wiki/Platform_as_a_service)). Feel free to refer to the [CMS documentation](https://docs.strapi.io/cms/intro) to learn more about Strapi CMS. - **Strapi Cloud Pricing Plans**
As a Strapi Cloud user you have the choice between 4 plans: Free, Essential, Pro and Scale. Depending on the plan, you have access to different functionalities, support and customization options (see [Pricing page](https://strapi.io/pricing-cloud) for more details). In this Strapi Cloud documentation, the , , and badges can be displayed below a section's title to indicate that the feature is only available starting from the corresponding paid plan. If no badge is shown, the feature is available on the Free plan. - **Types of Strapi Cloud users**
There can be 2 types of users on a Strapi Cloud project: owners and maintainers. The owner is the one who has created the project and has therefore access to all features and options for the project. Maintainers are users who have been invited to contribute to an already created project by its owner. Maintainers, as documented in the [Collaboration](/cloud/projects/collaboration) page, cannot view and access all features and options from the Strapi Cloud dashboard. - **Support**
The level of support provided by the Strapi Support team depends on the Strapi Cloud plan you subscribed for. The Free plan does not include access to support. The Essential and Pro plans include Basic support while the Scale plan includes Standard support. Please refer to the [dedicated support article](https://support.strapi.io/support/solutions/articles/67000680833-what-is-supported-by-the-strapi-team#Not-Supported) for all details regarding support levels. - **API access in Strapi Cloud vs self-hosted**
The REST and GraphQL APIs behave the same on Strapi Cloud and on self-hosted servers. The only differences are the URLs: - Base API domain: On Strapi Cloud, your API uses the domain of the environment (e.g. `https://.strapiapp.com/api/...`), or your custom domain if you set one (see [Domains documentation](/cloud/projects/settings#domains)). A self-hosted project would use whatever domain you expose. - Media Library URLs: Media fields in REST and GraphQL responses from Strapi Cloud always use the project media domain (e.g. `.media.strapiapp.com`), even when you access the API through a custom domain. Self-hosted projects return URLs from the configured upload provider, so the domain can match your own site or CDN. When you move a project from self-hosted to Strapi Cloud, make sure your frontend reads the absolute URLs returned by the API or accepts the Strapi Cloud media domain. # Cloud caching & performance Source: https://docs.strapi.io/cloud/getting-started/caching # Cloud caching & performance Edge caching via Cache-Control headers reduces latency and server load for heavy static content. For Strapi Cloud applications with large amounts of cacheable content, such as images, videos, and other static assets, enabling CDN (Content Delivery Network) caching via the [`Cache-control` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) can help improve application performance. CDN caching can help improve application performance in a few ways: * **Reducing Latency**: Caching frequently accessed content on edge servers located closer to the end-users can reduce the time it takes to load content. * **Offloading Origin Server**: By caching content on edge servers it can offload the origin server, reducing the load and allowing it to focus on delivering more dynamic content. * **Handling Traffic Spikes**: Help handle traffic spikes by distributing the load across multiple edge servers. This can prevent the origin server from becoming overwhelmed during peak traffic times and ensures a consistent user experience. ## Cache-Control Header in Strapi Cloud Static sites deployed on Strapi Cloud include, by default, a `Cache-Control` header set to cache for 24 hours on CDN edge servers and 10 seconds in web browsers. This is done to ensure that the latest version of the site is always served to users. Responses from dynamic apps served by Strapi Cloud are not cached by default. To enable caching, you must set the `Cache-Control` header in the app’s `HTTP` response functions. ```js function myHandler(req, res) { // Set the Cache-Control header to cache responses for 1 day res.setHeader('Cache-Control', 'max-age=86400'); // Add your logic to generate the response here } ``` ```ts function myHandler(req: Request, res: Response) { // Set the Cache-Control header to cache responses for 1 day res.setHeader('Cache-Control', 'max-age=86400'); // Add your logic to generate the response here } ``` # Strapi Cloud - Dashboard deployment Source: https://docs.strapi.io/cloud/getting-started/deployment # Project deployment with the Cloud dashboard Deploy your Strapi project on Strapi Cloud using the dashboard by choosing a plan, connecting a Git repository, configuring your project settings, and setting up billing details. This is a step-by-step guide for deploying your project on Strapi Cloud for the first time, using the Cloud dashboard. :::prerequisites Before you can deploy your Strapi application on Strapi Cloud using the Cloud dashboard, you need to have the following prerequisites: * Strapi version `4.8.2` or higher * Project database must be compatible with PostgreSQL. Strapi does not support and does not recommend using any external databases, though it's possible to configure one (see [advanced database configuration](/cloud/advanced/database)). * Project source code hosted on [GitHub](https://github.com) or [GitLab](https://about.gitlab.com/). The connected repository can contain multiple Strapi applications. Each Strapi app must be in a separate directory. * Specifically for GitLab: at least have "[Maintainer](https://docs.gitlab.com/ee/user/permissions.html)" permissions for the project to import on Strapi Cloud. ::: ## Logging in to Strapi Cloud 1. Navigate to the [Strapi Cloud](https://cloud.strapi.io) login page. 2. You have the options to log in with **GitHub**, **Google**, **GitLab** or via **One Time Password**. Choose your preferred option and log in. This initial login will create your Strapi Cloud account. Once logged in, you will be redirected to the Strapi Cloud *Projects* page where you can create your first Strapi Cloud project. ## Creating a project {#deploying-a-project} 1. From the *Projects* page, click the **Create project** button. 2. You will be redirected to the project creation interface. This interface contains 3 steps: choosing a plan, connecting a remote git repository, and setting up the project. 3. Choose a plan and a billing period for your Strapi Cloud project (see [Pricing](https://strapi.io/pricing-cloud) for details). 4. Connect a git repository to your new Strapi Cloud project. You may first have to select a git provider. If you have already deployed a project with one git provider, you can afterward deploy another project using another provider by clicking on the **Switch git provider** button and selecting either GitHub or GitLab. :::strapi Choose your path for your new Strapi Cloud project! Select one of the tabs below depending on how you wish to proceed: - by deploying a prebuilt Strapi template *(recommended for new users and beginners — only available on GitHub)*, - or by deploying your existing Strapi project. ::: 4.a. After connecting your GitHub account, click on the **Use template** button. 4.b. In the *Create repository with template* modal, choose the GitHub account where the repository will be created 4.c. Click on the **Create repository** button. A modal will confirm the creation of the repository. 4.d. If you have already given Strapi Cloud access to all repositories of your GitHub account, go directly to the next step. If not, you will be redirected to a GitHub modal where you will have to allow Strapi Cloud access to the newly created repository (more information in the [GitHub documentation](https://docs.github.com/en/apps/overview)). 4.e. Back in the project creation interface, the *Account* and *Repository* fields now match the newly created template. :::tip Connect the GitHub or GitLab account that owns the repository you want to deploy. This can be different from the account you used to log into your Strapi Cloud account. ::: :::note You can only connect a GitHub organization repository on a paid plans. On the free plan, you can only connect a personal repository. ::: 4.a. If you have already given Strapi Cloud access to all repositories of your GitHub or GitLab account, go directly to the next step. If not, you will be redirected to a modal where you will have to allow Strapi Cloud permission to access some or all your repositories on GitHub/GitLab (more information in the [GitHub](https://docs.github.com/en/apps/overview) and [GitLab](https://docs.gitlab.com/ee/integration/oauth_provider.html#view-all-authorized-applications) documentation). 4.c. Back in the project creation interface, select the *Account* and the *Repository* you want to deploy. 5. Set up your Strapi Cloud project. 5.a. Fill in the following information: | Setting name | Instructions | |--------------|---------------------------------------------------------------------------------------------------------| | Display name | The name is automatically populated based on the repository you selected, but you can edit it if needed. | | Git branch | Choose from the drop-down the branch you want to deploy. | | Deploy on push | Tick this box to automatically trigger a deployment when changes are pushed to your selected branch. When disabled, you will need to manually deploy the latest changes. | | Region | Choose the geographic location of the servers where your Strapi application is hosted. Selected region can either be US (East), Europe (West) or Asia (Southeast). | :::note The Git branch and "Deploy on push" settings can be modified afterwards through the project settings. However, the hosting region can only be chosen during the creation of the project (see [Project Settings](/cloud/projects/settings)). ::: 5.b. (optional) Click on **Show advanced settings** to fill in the following options: | Setting name | Instructions | |--------------|---------------------------------------------------------------------------------------------------------| | Base directory | Write the name of the directory where your Strapi app is located in the repository. This is useful if you have multiple Strapi apps in the same repository or if you have a monorepo. | | Environment variables | Click on **Add variable** to add environment variables used to configure your Strapi app (see [Environment variables](/cms/configurations/environment/) for more information). You can also add environment variables to your Strapi application by adding a `.env` file to the root of your Strapi app directory. The environment variables defined in the `.env` file will be used by Strapi Cloud. | | Node version | Choose a Node version from the drop-down. The default Node version will automatically be chosen to best match the version of your Strapi project. If you manually choose a version that doesn't match with your Strapi project, the build will fail but the explanation will be displayed in the build logs. | :::strapi Using Environment Variables You can use environment variable to connect your project to an external database rather than the default one used by Strapi Cloud (see [database configuration](/cms/configurations/database#environment-variables-in-database-configurations) for more details). If you would like to revert and use Strapi's default database again, remove your `DATABASE_` environment variables (no automatic migration implied). You can also set up here a custom email provider. Sendgrid is set as the default one for the Strapi applications hosted on Strapi Cloud (see [providers configuration](/cms/features/email#providers) for more details). ::: ## Setting up billing details :::strapi No billing step for the Free plan If you chose the free plan, this billing step will be skipped as you will not be asked to share your credit card details at the creation of the project. Skip to step 5 of the section below to finalize the creation of your project. ::: 1. Click on the **Continue to billing** button. You will be redirected to the billing page where you can enter your payment details and review your invoice. 2. In the *Payment method* section, add a credit card. This card will be used for all project-related transactions, including add-ons and overages. 3. In the *Billing information* section, fill in your payment details and billing address. 4. Review the *Invoice* section. When purchasing a monthly subscription, the subscription price will be prorated for the remaining days in the current billing cycle. Optionally, expand the *Discount code* section to enter a code. :::note Taxes may be added to your invoice based on your billing address: - In the EU, UK, Canada, and India, providing a valid VAT ID exempts you from VAT. If no valid VAT ID is provided, VAT will be added to your invoice. - In the US, applicable sales taxes are calculated based on your state and address. ::: 5. Click on the **Subscribe** button to finalize the creation of your new Strapi Cloud project. ## Deploying your project After confirming the project creation, you will be redirected to your *Project dashboard* where you will be able to follow its creation and first deployment. While your project is deploying, you can already start configuring some of your [project settings](/cloud/projects/settings). :::note If an error occurs during the project creation, the progress indicator will stop and display an error message. You will see a **Retry** button next to the failed step, allowing you to restart the creation process. ::: Once your project is successfully deployed, the creation tracker will be replaced by your deployments list and you will be able to visit your Cloud hosted project. Don't forget to create the first Admin user before sharing your Strapi project. ## What to do next? Now that you have deployed your project via the Cloud dashboard, we encourage you to explore the following ideas to have an even more complete Strapi Cloud experience: - Invite other users to [collaborate on your project](/cloud/projects/collaboration). - Check out the [deployments management documentation](/cloud/projects/deploys) to learn how to trigger new deployments for your project. # Strapi Cloud - CLI deployment Source: https://docs.strapi.io/cloud/getting-started/deployment-cli # Project deployment with the Command Line Interface (CLI) Deploy a Strapi project to Strapi Cloud using the `strapi login` and `strapi deploy` CLI commands, with optional automatic deployment on git repository commits. This is a step-by-step guide for deploying your project on Strapi Cloud for the first time, using the Command Line Interface. :::prerequisites Before you can deploy your Strapi application on Strapi Cloud using the Command Line Interface, you need to have the following prerequisites: - Have a Google, GitHub or GitLab account. - Have an already created Strapi project (see [Installing from CLI in the CMS Documentation](/cms/installation/cli)), stored locally. The project must be less than 100MB. - Have available storage in your hard drive where the temporary folder of your operating system is stored. ::: ## Logging in to Strapi Cloud 1. Open your terminal. 2. Navigate to the folder of your Strapi project, stored locally on your computer. 3. Enter the following command to log into Strapi Cloud: ```bash yarn strapi login ``` ```bash npx run strapi login ``` 4. In the browser window that opens automatically, confirm that the code displayed is the same as the one written in the terminal message. 5. Still in the browser window, choose whether to login via Google, GitHub or GitLab. The window should confirm the successful login soon after. ## Deploying your project 1. From your terminal, still from the folder of your Strapi project, enter the following command to deploy the project: ```bash yarn strapi deploy ``` ```bash npx run strapi deploy ``` 2. Follow the progression bar in the terminal until confirmation that the project was successfully deployed with Strapi Cloud. Deploying the project will create a new Strapi Cloud project on the Free plan. ### Automatically deploying subsequent changes By default, when creating and deploying a project with the Cloud CLI, you need to manually deploy again all subsequent changes by running the corresponding `deploy` command everytime you make a change. Another option is to enable automatic deployment through a git repository. To do so: 1. Host your code on a git repository, such as [GitHub](https://www.github.com) or [GitLab](https://www.gitlab.com). 2. Connect your Strapi Cloud project to the repository (see the _Connected repository_ setting in [Projects Settings > General](/cloud/projects/settings#general)). 3. Still in _Projects Settings > General_ tab, tick the box for the "Deploy the project on every commit pushed to this branch" setting. From now on, a new deployment to Strapi Cloud will be triggered any time a commit is pushed to the connected git repository. :::note Automatic deployment is compatible with all other deployment methods, so once a git repository is connected, you can trigger a new deployment to Strapi Cloud [from the Cloud dashboard](/cloud/projects/deploys), [from the CLI](/cloud/cli/cloud-cli#strapi-deploy), or by pushing new commits to your connected repository. ::: ## What to do next? Now that you have deployed your project via the Command Line Interface, we encourage you to explore the following ideas to have an even more complete Strapi Cloud experience: - Visit the Cloud dashboard to follow [insightful metrics and information](/cloud/projects/overview) on your Strapi project. - Check out the full [Command Line Interface documentation](/cloud/cli/cloud-cli) to learn about the other commands available. # Cloud project deployment Source: https://docs.strapi.io/cloud/getting-started/deployment-options # Project deployment with Strapi Cloud Deploy your Strapi application on Strapi Cloud using either the Cloud dashboard or the CLI, with step-by-step guides provided for both methods. You have 2 options to deploy your project with Strapi Cloud: - either with the user interface (UI), meaning that you will perform all the actions directly on the Strapi Cloud dashboard, - or using the Cloud Comment Line Interface (CLI), meaning that you will only interact with a terminal. The guides below will guide you through all the steps for each of the deployment options. - [Via the Cloud dashboard](/cloud/getting-started/deployment): Step-by-step guide to create and deploy a project via the user interface. - [Via the CLI](/cloud/getting-started/deployment-cli): Step-by-step guide to create and deploy a project with the Cloud Command Line Interface. # Cloud billing & usage Source: https://docs.strapi.io/cloud/getting-started/usage-billing # Cloud billing & usage Strapi Cloud offers four plans (Free, Essential, Pro, Scale) with usage-based pricing that varies by API requests, asset storage, and bandwidth, plus overages charged monthly; projects may be suspended for unpaid invoices or plan violations. This page contains general information related to the usage and billing of your Strapi Cloud account and projects. Strapi Cloud offers 1 Free plan and 3 paid plans: Essential, Pro and Scale (see [Pricing page](https://strapi.io/pricing-cloud)). The table below summarizes Strapi Cloud usage-based pricing plans, for general features and usage: | Feature | Free | Essential | Pro | Scale | | -------------------------------- | ----- | --------- | --- | ----- | | **Database Entries** | 500 | Unlimited* | Unlimited* | Unlimited* | | **Asset Storage** | 10GB | 50GB | 250GB | 1,000GB | | **Asset Bandwidth (per month)** | 10GB | 50GB | 500GB | 1,000GB | | **API Requests (per month)** | 2,500 | 50,000 | 1,000,000 | 10,000,000 | | | | | | | | **Backups** | N/A | N/A | Weekly | Daily | | **Custom domains** | N/A | Included | Included | Included | | **Environments** | N/A | N/A | 0 included (up to 99 extra) | 1 included (up to 99 extra) | | **Emails (per month)** | 100 | Unlimited* | Unlimited* | Unlimited* | :::strapi Additional information on usage and features - General features & usage: - Database entries are the number of entries in your database. - Asset storage is the amount of storage used by your assets. - Asset bandwidth is the amount of bandwidth used by your assets. - API requests are the number of requests made to your APIs. This includes requests to the GraphQL and REST APIs, excluding requests for file and media assets counted towards CDN bandwidth and storage. All API requests are counted towards your monthly usage, regardless of the response type. - Cloud specific feature: - Backups refers to the automatic backups of Strapi Cloud projects (see [Backups documentation](/cloud/projects/settings#backups) for more information on the feature). - Custom domains refer to the ability to define a custom domain for your Strapi Cloud (see [Custom domains](/cloud/projects/settings#connecting-a-custom-domain)). - Environments refers to the number of environments included in the plan on top of the default production environment (see [Environments](/cloud/projects/settings#environments) documentation for more information on the feature). ::: :::caution The Free Plan is for personal, non-commercial use only Commercial use means any project made for financial gain, including: - Accepting or processing payments on your site - Accepting or receiving payment to create, update, or host the site - Advertising, promoting, or selling products or services - Hosting or serving advertisements for any 3rd party For more information, visit [Strapi Cloud-Legal](https://strapi.io/cloud-legal) ::: :::info Scale-to-zero and cold start on the Free plan On the Free plan, projects automatically scale down to zero after a short period of inactivity. When the application is accessed again—either through the frontend or via an API request—it may take a few seconds (up to a minute) before a response is returned. Upgrading to a paid plan disables scaling to zero and cold starts, resulting in instant response times at all times. ::: ## Environments management Environments are isolated instances of your Strapi Cloud project. All projects have a default production environment, but other additional environments can be configured for projects on a Pro or Scale plan, from the *Environments* tab of the project settings (see [Environments](/cloud/projects/settings#environments)). There is no limit to the number of additional environments that can be configured for a Strapi Cloud project. The usage limits of additional environments are the same as for the project's production environment (e.g. an additional environment on the Pro plan will be limited at 250GB for asset storage, and overages will be charged the same way as for the production environment). Note however that the asset bandwidth and API calls are project-based, not environment-based, so these usage limits do not change even with additional environments. ## Billing Billing is based on the usage of your Strapi Cloud projects. Project plans and addons are either billed monthly or yearly, depending on your billing cycle, while overages are billed monthly. You can view your usage and billing information in the *Billing & Usage* section of your project settings. ### Taxes For billing addresses in the US, UK, Canada, India, and EU, local taxes may be added to your invoices. Tax amounts are calculated based on your billing address and VAT/Tax ID status, and are displayed during checkout and on invoices. You can add or update your VAT/Tax ID from your [Account Billing](/cloud/account/account-billing) settings. ### Overages :::caution Overages are not allowed on the Free plan. ::: If you exceed the limits of your plan for API Requests, Asset Bandwidth, or Asset Storage, you will be charged for the corresponding overages. For example, if you exceed the 500GB limit in asset bandwidth of the Pro plan, you will be charged for the excess bandwidth at the end of the current billing period or on project deletion. Overages are not prorated and are charged in full. Overages are charged monthly, according to the following rates: | Feature | Rate | | --- | --- | | **API Requests** | $1.50 / 25k requests | | **Asset Bandwidth** | $30.00 / 100GB | | **Asset Storage** | $0.60 / GB per month | ### Project suspension Projects may end up in a **Suspended** state for various reasons, including: unpaid invoices, exceeding the limits of your free plan, or violating Strapi Cloud's [terms of service](https://strapi.io/cloud-legal). If your project is suspended, you will no longer be able to access the Strapi admin panel, nor trigger new deployments. A banner will appear in your project's dashboard, indicating the cause of the suspension. You will also be notified by email. #### Project suspension for exceeding the Free plan limits When a project hosted with the Free plan exceeds either the API requests or the Asset Bandwidth limits, it will be suspended until the monthly allowance resets at the beginning of the following month. While the project is suspended: - Users cannot trigger new deployments - Access to the application is blocked - Users cannot make changes to the project’s settings To reactivate the project immediately, users can upgrade to a paid plan. #### Project suspension due to billing issues If you have unpaid invoices, the subscription of your project will automatically be canceled and the project suspended. To reactivate your project subscription: 1. Click the **Pay now** button in the project banner, or in *Settings > Billing & Usage* 2. Pay your overdue invoice(s) on the external payment page 3. Wait up to 1 minute for your project to reactivate :::warning If you do not resolve the issue within 30 days, your suspended project will be deleted and all its data will be permanently lost. ::: #### Project suspension for other reasons If your project was suspended for reasons other than unpaid invoice leading to subscription cancellation, you may not have the possibility to reactivate your project yourself. You should receive an email with instructions on how to resolve the issue. If you do not receive the email notification, please contact the [Strapi Support platform](https://support.strapi.io/support/home). ### Subscription cancellation If you want to cancel your Strapi Cloud subscription, you have 2 options: - either change your project's subscription to the free plan (see [Downgrading to another plan](/cloud/projects/settings#downgrading-to-another-plan) documentation), - or completely delete your project (see [Deleting Strapi Cloud project](/cloud/projects/settings#deleting-a-strapi-cloud-project) documentation). # Welcome to the Strapi Cloud Documentation! Source: https://docs.strapi.io/cloud/intro # Welcome to the Strapi Cloud Documentation! Strapi Cloud is a fully managed hosting platform for deploying Strapi applications, abstracting infrastructure complexity while enabling content management, collaboration, and compliance features. The Strapi Cloud documentation contains all information related to the setup, deployment, update and customization of your Strapi Cloud account and applications. :::strapi What is Strapi Cloud? [Strapi Cloud](https://strapi.io/cloud) is a hosting platform that allows you to deploy your Strapi applications. It is a fully managed content platform **🤝 Why Strapi Cloud?**
Strapi Cloud enables you to increase your content velocity without having to compromise on customization needs and requirements.
Development teams can rely on Strapi Cloud to abstract away the complexity of infrastructure management while keeping your development workflow and extending the core capabilities of Strapi.
Content managers can use Strapi Cloud to autonomously manage all types of content and benefit from a complete set of content collaboration, security, and compliance features. built on top of Strapi, the open-source headless CMS. ::: :::prerequisites The typical workflow, which is recommended by the Strapi team, is: 1. Create your Strapi application locally (v4.8.2 or later). 2. Optionally, extend the application with plugins or custom code. 3. Version the application's codebase through your git provider (GitHub or GitLab). 4. Deploy the application with Strapi Cloud. ::: The Strapi Cloud documentation is organised in topics in a order that should correspond to your journey with the product. The following cards, on which you can click, will redirect you to the main topics and steps. - [Project creation](/cloud/getting-started/deployment): Step-by-step guide to guide you through the creation and deployment of a Strapi Cloud project. - [Information on billing & usage](/cloud/getting-started/usage-billing): All details on Strapi Cloud plans & billing, including overages and project suspension. - [Projects overview](/cloud/projects/overview): Information on how to access Strapi Cloud projects to view their details & usage, and manage them. - [Projects settings](/cloud/projects/settings): Details on all the available settings for Strapi Cloud projects and how to configure them. - [Collaboration](/cloud/projects/collaboration): Documentation for the Collaboration feature to invite other users to access and manage a project. - [Deployments management](/cloud/projects/deploys): All details on the deployment of a Strapi Cloud project, including triggering or cancelling a deployment. - [Account billing & details](/cloud/account/account-billing): Information on Strapi Cloud subscriptions and how to manage, edit and cancel them. :::strapi Welcome to the Strapi community! Strapi Cloud is built on top of Strapi, an open-source, community-oriented project. The Strapi team has at heart to share their vision and build the future of Strapi with the Strapi community. This is why the [roadmap](https://feedback.strapi.io) is open: as all insights are very important and will help steer the project in the right direction. Any community member is most welcome to share ideas and opinions there. You can also join [GitHub](https://github.com/strapi/strapi), [GitHub Discussions](https://github.com/strapi/strapi/discussions), and the [Discord](https://discord.strapi.io) and benefit from the years of experience, knowledge, and contributions by the Strapi community as a whole. ::: :::tip Need help? Strapi Cloud customers on eligible plans can contact the Strapi team through the [Strapi Support platform](https://support.strapi.io/support/home). Community users can ask questions on [GitHub Discussions](https://github.com/strapi/strapi/discussions) or [Discord](https://discord.strapi.io). Support level depends on your plan (see the [Cloud fundamentals](/cloud/cloud-fundamentals) page). ::: # Cloud project collaboration Source: https://docs.strapi.io/cloud/projects/collaboration # Cloud project collaboration Project owners invite maintainers through the Share button, manage pending invitations, and revoke access. Projects are created by a user via their Strapi Cloud account. Strapi Cloud users can share their projects to anyone else, so these new users can have access to the project dashboard and collaborate on that project, without the project owner to ever have to share their credentials. Users invited to collaborate on a project, called maintainers, do not have the same permissions as the project owner. Contrary to the project owner, maintainers: - Cannot share the project themselves to someone else - Cannot delete the project from the project settings - Cannot access the *Billing* section of project settings ## Sharing a project To invite a new maintainer to collaborate on a project: 1. From the *Projects* page, click on the project of your choice to be redirected to its dashboard. 2. Click on the **Share** button located in the dashboard's header. 3. In the *Share [project name]* dialog, type the email address of the person to invite in the textbox. A dropdown indicating "Invite [email address]" should appear. 4. Click on the dropdown: the email address should be displayed in a purple box right below the textbox. 5. (optional) Repeat steps 3 and 4 to invite more people. Email addresses can only entered one by one but invites can be sent to several email addresses at the same time. 6. Click on the **Send** button. New maintainers will be sent an email containing a link to click on to join the project. Once a project is shared, avatars representing the maintainers will be displayed in the project dashboard's header, next to the **Share** button, to see how many maintainers collaborate on that project and who they are. :::tip Avatars use GitHub, Google or GitLab profile pictures, but for pending users only initials will be displayed until the activation of the maintainer account. You can hover over an avatar to display the full name of the maintainer. ::: ## Managing maintainers From the *Share [project name]* dialog accessible by clicking on the **Share** button of a project dashboard, projects owners can view the full list of maintainers who have been invited to collaborate on the project. From there, it is possible to see the current status of each maintainer and to manage them. Maintainers whose full name is displayed are users who did activate their account following the invitation email. If however there are maintainers in the list whose email address is displayed, it means they haven't activated their accounts and can't access the project dashboard yet. In that case, a status should be indicated right next to the email address to explain the issue: - Pending: the invitation email has been sent but the maintainer hasn't acted on it yet. - Expired: the email has been sent over 72 hours ago and the invitation expired. For Expired statuses, it is possible to send another invitation email by clicking on the **Manage** button, then **Resend invite**. ### Revoking maintainers To revoke a maintainer's access to the project dashboard: 1. Click on the **Share** button in the project dashboard's header. 2. In the list of *People with access*, find the maintainer whose access to revoke and click on the **Manage** button. 3. Click on the **Revoke** button. 4. In the confirmation dialog, click again on the **Revoke** button. The revoked maintainer will completely stop having access to the project dashboard. :::note Maintainers whose access to the project has been revoked do not receive any email or notification. ::: # Cloud deployments management Source: https://docs.strapi.io/cloud/projects/deploys # Cloud deployments management Deployment triggers can be manual or automatic on git pushes, with the ability to cancel active builds from dashboard or CLI. The creation of a new Strapi Cloud project automatically trigger the deployment of that project. After that, deployments can be: - manually triggered whenever needed, [from the Cloud dashboard](#triggering-a-new-deployment) or [from the CLI](/cloud/cli/cloud-cli#strapi-deploy), - or automatically triggered everytime a new commit is pushed to the branch, if the Strapi Cloud project is connected to a git repository and the "deploy on push" option is enabled (see [Project settings](/cloud/projects/settings#modifying-git-repository--branch)). Ongoing deployments can also be [manually canceled](#cancelling-a-deployment) if needed. ## Triggering a new deployment To manually trigger a new deployment for your project, click on the **Trigger deployment** button always displayed in the right corner of a project dashboard's header. This action will add a new card in the *Deployments* tab, where you can monitor the status and view the deployment logs live (see [Deploy history and logs](/cloud/projects/deploys-history)). ## Cancelling a deployment If for any reason you want to cancel an ongoing and unfinished deployment: 1. Go to the *Deployment details* page of the latest triggered deployment (see [Accessing log details](/cloud/projects/deploys-history#accessing-deployment-details--logs)). 2. Click on the **Cancel deployment** button in the top right corner. The status of the deployment will automatically change to *Canceled*. :::tip You can also cancel a deployment from the *Deployments* tab which lists the deployments history. The card of ongoing deployment with the *Building* status will display a ![Cancel button](/img/assets/icons/clear.svg) button for cancelling the deployment. ::: # Cloud deployment history & logs Source: https://docs.strapi.io/cloud/projects/deploys-history # Cloud deployment history and logs {#deploy-history-and-logs} Deployments tab lists every build with status and allows deep inspection of build and deployment logs. For each Strapi Cloud project, you can access the history of all deployments that occurred and their details including build and deployment logs. This information is available in the *Deployments* tab. ## Viewing the deployment history {#viewing-deploy-history} In the *Deployments* tab is displayed a chronological list of cards with the details of all historical deployments for your project. Each card displays the following information: - Commit SHA 💡 The commit SHA (or hash) is the unique ID of your commit, which refers to a specific change that was made at a specific time., with a direct link to your git provider, and commit message - Deployment status: - *Deploying* - *Done* - *Canceled* - *Build failed* - *Deployment failed* - Last deployment time (when the deployment was triggered and the duration) - Branch ## Accessing deployment details & logs From the *Deployments* tab, you can hover a deployment card to make the ![See logs button](/img/assets/icons/Eye.svg) **Show details** button appear. Clicking on this button will redirect you to the *Deployment details* page which contains the deployment's detailed logs. In the *Build logs* and *Deployment logs* sections of the page you can click on the arrow buttons ![Down arrow](/img/assets/icons/ONHOLDCarretDown.svg) ![Up arrow](/img/assets/icons/ONHOLDCarretUp.svg) to show or hide the build and deployment logs of the deployment. :::tip Click the ![Copy button](/img/assets/icons/duplicate.svg) **Copy to clipboard** button to copy the log contents. ::: In the right side of the *Deployment details* page is also displayed the following information: - *Commit*: the commit SHA 💡 The commit SHA (or hash) is the unique ID of your commit, which refers to a specific change that was made at a specific time., with a direct link to your git provider, and commit message used for this deployment - *Status*, which can be *Building*, *Deploying*, *Done*, *Canceled*, *Build failed*, or *Deployment failed* - *Source*: the branch and commit message for this deployment - *Duration*: the amount of time the deployment took and when it occurred # Cloud project logs Source: https://docs.strapi.io/cloud/projects/logs # Cloud project logs The Logs tab streams your project's live logs in a searchable, filterable table. Click any entry to inspect its full message and metadata. From the project dashboard, the *Logs* tab streams the live logs of the project as structured entries you can search, filter, and inspect. :::note The *Logs* page is only accessible once the project has a successful deployment and is inaccessible during major environment operations, such as project creation, data transfer, or environment clearing. ::: ## Viewing logs The viewer follows the live logs stream and auto-scrolls to keep the latest entries in view. Each row shows three columns: | Column | Description | | --- | --- | | Timestamp | When the log was emitted. | | Type | The log level, *Error*, *Warning*, *Info*, or *HTTP*, shown as a colored badge. For *HTTP* entries, the response status code is shown next to the badge. | | Message | The log message. Long messages are truncated in the table; click the entry to read the full text in the drawer. | You can copy a single log line by clicking the copy button visible on row hover, or within the drawer. To copy every entry currently shown in the log viewer, use the copy button in the toolbar instead. :::caution The live log stream is currently limited to the last 15 minutes and capped at 100,000 rows. Historical log visibility is under development. ::: ## Inspecting a log entry Click any log row to open a detail drawer and display the full message and, when available, the *Metadata* of the log. The *Metadata* section shows the following information: | Field | Description | | --- | --- | | `method` | HTTP method (e.g. `GET`, `POST`). | | `path` | Requested path. | | `route` | Matched application route. | | `status_code` | HTTP response status code. | | `error_type` | Error classification, when the entry is an error. | | `duration_ms` | Time taken to handle the request, in milliseconds. | | `response_size` | Size of the response. | | `request_type` | Type of request (e.g. API, admin). | ## Searching and filtering logs You can use the search and filter tools to refine the logs displayed in the viewer. Filters and search combine, so you can, for example, show only *HTTP* entries with a *5xx* status that mention a specific route. :::note Projects deployed before the structured viewer was introduced display their logs as plain text, without search, filtering, or per-entry metadata. To access the new log viewer, trigger a manual redeployment of the project. ::: ### Search bar Type in the search field to keep only the entries whose message contains your text. ### Type filter Filter by the following log levels: - Error - Warning - Info - HTTP Error logs are highlighted in red so they stand out as you scan. ### HTTP status code filter Filter by the following status codes: - 2xx - 3xx - 4xx - 5xx # Cloud notifications Source: https://docs.strapi.io/cloud/projects/notifications # Cloud notifications Bell icon opens a feed of recent deployment events, automatically purged after 30 days. The Notification center can be opened by clicking the bell icon in the top navigation of the Cloud dashboard. It displays a list of the latest notifications for all your existing projects. Clicking on a notification card from the list will redirect you to the *Log details* page of the corresponding deployment (more information in [Deploy history & logs](/cloud/projects/deploys-history#accessing-deployment-details--logs)). The following notifications can be listed in the Notifications center: - *deployment completed*: when a deployment is successfully done. - *Build failed*: when a deployment fails during the build stage. - *deployment failed*: when a deployment fails during the deployment stage. - *deployment triggered*: when a deployment is triggered by a new push to the connected repository. This notification is however not sent when the deployment is triggered manually. :::note All notifications older than 30 days are automatically removed from the Notification center. ::: # Cloud projects overview Source: https://docs.strapi.io/cloud/projects/overview # Cloud projects overview Projects page lists all apps with status and quick actions; selecting one opens a dashboard with metrics and controls. The *Projects* page displays a list of all your Strapi Cloud projects. From here you can manage your projects and access the corresponding applications. Each project card displays the following information: * the project name * the last successful deployment’s date of the Production environment * the current status of the project: * *Disconnected*, if the project repository is not connected to Strapi Cloud * *Suspended*, if the project has been suspended (refer to [Project suspension](/cloud/getting-started/usage-billing#project-suspension) to reactivate the project) * *Incompatible version*, if the project is using a Strapi version that is not compatible with Strapi Cloud Each project card also displays a menu icon to access the following options: * **Visit App**: to be redirected to the application * **Go to Deployments**: to be redirected to the [*Deployment*](/cloud/projects/deploys) page * **Go to Settings**: to be redirected to the [*Settings*](/cloud/projects/settings) page :::tip Click on the * Product updates* button in the navigation bar to check out the latest features and fixes released. ::: ## Accessing a project's dashboard From the *Projects* page, click on any project card to access its dashboard. It displays the project and environment details and gives access to the deployment history and all available settings. From the dashboard's header of a chosen project, you can: - use the **Share** button to invite users to collaborate on the project (see [Collaboration](/cloud/projects/collaboration)) and see the icons of those who have already been invited , - use the **Settings** button to access the settings of the project and its existing environments , - choose which environment to visualise for the project or add a new environment , - trigger a new deployment (see [Deployments management](/cloud/projects/deploys)) and visit your application . Your project's dashboard also displays: - the *Deployments* and *Logs* tabs, to see the deployments history (more details in [Deploy history and logs](/cloud/projects/deploys-history)) and the logs of the project (see [dedicated documentation page](/cloud/projects/logs)) - the project and environment details in a box on the right of the interface , including: - the number of API calls, - the current usage for asset bandwidth and storage, - the name of the branch and a **Manage** button to be redirect to the branch settings (see [Modifying git repository & branch](/cloud/projects/settings#modifying-git-repository--branch)), - the name of the base directory, - the Strapi version number, - the Strapi app's url. # Cloud project settings Source: https://docs.strapi.io/cloud/projects/settings # Cloud project settings Settings area spans project-level controls (general, billing, plans, invoices) and per-environment configuration. From a chosen project's dashboard, the **Settings** button, located in the header, enables you to manage the configurations and settings for your Strapi Cloud project and its environments. The settings' menu on the left side of the interface is separated into 2 categories: the settings for the entire project and the settings specific to any configured environment for the project. ## Project-level settings There are 5 tabs available for the project settings: - [*General*](#general), - [*Environments*](#environments), - [*Billing & Usage*](#billing--usage), - [Plans](#plans), - and [Invoices](#invoices). ### General The *General* tab for the project-level settings enables you to check and update the following options for the project: - *Basic information*, to see: - the name of your Strapi Cloud project — used to identify the project on the Cloud Dashboard, Strapi CLI, and deployment URLs — and change it (see [Renaming project](#renaming-project)). - the chosen hosting region for your Strapi Cloud project, meaning the geographical location of the servers where the project and its data and resources are stored. The hosting region is set at project creation (see [Project creation](/cloud/getting-started/deployment)) and cannot be modified afterwards. - the project's metadata, including the Production app internal name and the Subscription ID, which can be useful for debugging & support purposes. - *Strapi CMS license key*: to enable and use some CMS features directly on your Cloud project (see [Pricing page](https://strapi.io/pricing-self-hosted) to purchase a license). - *Connected Git repository*: to change the repository and branch used for your project (see [Modifying git repository & branch](#modifying-git-repository--branch)). Also allows to enable/disable the "deploy on push" option. - *Danger zone*, with: - *Transfer ownership*: for the project owner to transfer the ownership of the Cloud project to an already existing maintainer (see [Transferring project ownership](#transferring-project-ownership)). - *Delete project*: to permanently delete your Strapi Cloud project (see [Deleting Strapi Cloud project](#deleting-a-strapi-cloud-project)). #### Renaming project The project name is set at project creation (see [Project creation](/cloud/getting-started/deployment)) and can be modified afterwards via the project settings. 1. In the *Basic information* section of the *General* tab, click on the edit button. 2. In the dialog, write the new project name of your choice in the *Project name* textbox. 3. Click on the **Rename** button to confirm the project name modification. #### Adding a CMS license key {#adding-cms-license-key} A CMS license key can be added and connected to a Strapi Cloud project to unlock additional Strapi CMS features across all of the project’s environments. The CMS features that will be accessible via the license key depend on the type of license that was purchased. Please refer to the [Strapi Pricing page](https://strapi.io/pricing-self-hosted) for more information and/or to purchase a license. :::note If you don't see the *Strapi CMS license key* section, it probably means that your subscription is a legacy one and does not support custom CMS licenses. It means that you already have one that is automatically included on your project. ::: 1. In the *Strapi CMS license key* section, click on the **Add license** button. 2. In the dialog, paste your license key in the field. 3. Click on the **Save & deploy** button for the changes to take effect. To remove the Strapi CMS license from your Strapi Cloud project, you can click on the **Unlink license** button. This will also remove access and usage to the CMS features included in the previously added license. :::note The license key is applied to all the environments in the project. ::: #### Modifying git repository & branch The GitHub or GitLab repository, branch and base directory for a Strapi Cloud project are by default chosen at the creation of the project (see [Creating a project](/cloud/getting-started/deployment)). After the project's creation, via the project settings, it is possible to update the project repository or switch to another git provider. :::caution Updating the git repository could result in the loss of the project and its data, for instance if the wrong repository is selected or if the data schema between the old and new repository doesn't match. ::: 1. In the *Connected git repository* section of the *General* tab, click on the **Update repository** button. You will be redirected to another interface. 2. (optional) If you wish to not only update the repository but switch to another git provider, click on the **Switch Git provider** button at the top right corner of the interface. You will be redirected to the chosen git provider's authorization settings before getting back to the *Update repository* interface. 3. In the *Update repository* section, fill in the 2 available settings: | Setting name | Instructions | | --------------- | ------------------------------------------------------------------------ | | Account | Choose an account from the drop-down list. | | Repository | Choose a repository from the drop-down list. | 4. In the *Select Git branches* section, fill in the available settings for any of your environments. Note that the branch can be edited per environment via its own settings, see [General (environment)](#environments). | Setting name | Instructions | | --------------- | ------------------------------------------------------------------------ | | Branch | Choose a branch from the drop-down list. | | Base directory | Write the path of the base directory in the textbox. | | Auto-deploy | Tick the box to automatically trigger a new deployment whenever a new commit is pushed to the selected branch. Untick it to disable the option. | 5. Click on the **Save & deploy** button for the changes to take effect. #### Transferring project ownership {#transferring-project-ownership} The ownership of the Strapi Cloud project can be transferred to another user, as long as they're a maintainer of the project. It can either be at the initiative of the current project owner, or can be requested by a project maintainer. Once the ownership is transferred, it is permanent until the new owner decides to transfer the ownership again to another maintainer. :::prerequisites For the ownership of a project to be transferred, the following requirements must be met: - The project must be on a paid plan, with no currently expired card and/or unpaid bills. - The maintainer must have filled their billing information. - No already existing ownership transfer must be pending for the project. Note that ownership transfers might fail when done the same day of subscription renewal (i.e. 1st of every month). If the transfer fails that day, but all prerequisites are met, you should wait a few hours and try again. ::: 1. In the *Danger zone* section of the *General* tab, click on the **Transfer ownership** button. 2. In the dialog: - If you are the project owner: choose the maintainer who should be transferred the ownership by clicking on **...** > **Transfer ownership** associated with their name. - If you are a maintainer: find yourself in the list and click on **...** > **Transfer ownership** associated with your name. 3. Confirm the transfer/request in the new dialog by clicking on the **Transfer ownership** button. An email will be sent to both users. The person who needs to transfer the ownership or inherit it will have to click on the **Confirm transfer** button in the email. Once done, the previous owner will receive a confirmation email that the transfer has successfully been done. :::tip As long as the ownership transfer or request hasn't been confirmed, there is the option to cancel in the same dialog that the maintainer was chosen. ::: :::note Once the ownership transfer is done, the project will be disconnected from Strapi Cloud. As new owner, make sure to go to the *General* tab of project settings to reconnect the project. ::: #### Deleting a Strapi Cloud project You can delete any Strapi Cloud project, but it will be permanent and irreversible. Associated domains, deployments and data will be deleted and the subscription for the project will automatically be canceled. 1. In the *Danger zone* section of the *General* tab, click on the **Delete project** button. 2. In the dialog, select the reason for deleting your project. 3. Confirm the deletion of your project by clicking on the **Delete project** button. ### Environments {#environments} The *Environments* tab allows to see all configured environments for the Strapi Cloud project, as well as to create new ones. Production is the default environment, which cannot be deleted. Other environments can be created (depending on the subscription plan for your project) to work more safely on isolated instances of your Strapi Cloud project (e.g. a staging environment where tests can be made before being available on production). :::note The billing cycle of additional environments you purchase will match the billing cycle of your plan. ::: To create a new environment: 1. Click on the **Add a new environment** button. 2. In the setup step, fill in the available settings: | Setting name | Instructions | | ---------------- | ------------------------------------------------------------------------ | | Environment name | (mandatory) Write a name for your project's new environment. | | Git branch | (mandatory) Select the right branch for your new environment. | | Base directory | Write the name of the base directory of your new environment. | | Deploy on push | Tick this box to automatically trigger a deployment when changes are pushed to your selected branch. When disabled, you will need to manually deploy the latest changes. | | Import variables | Tick the box to import variable names from an existing environment. Values will not be imported, and all variables will remain blank. | 3. Click **Confirm** to proceed to the checkout step. 4. Review the environment price, applicable taxes and proration adjustment. 5. Click on the **Add environment** button to create your project's new environment. You will then be redirected to your *Project dashboard* where you will be able to follow your new environment's creation and first deployment. :::note If an error occurs during the environment creation, the progress indicator will stop and display an error message. You will see a **Retry** button next to the failed step, allowing you to restart the creation process. ::: ### Billing & Usage The *Billing & Usage* tab displays your next estimated payment, all information on the current subscription plan and a detailed summary of the project's usage. It also allows you to add new environments (please [refer to the documentation in the Environments section](#environments)) for your project. Through this tab, you also have the possibility to: - click the **Change** button to be redirected to the *Plans* tab, where you can change your subscription plan or billing cycle ([see related documentation](#plans)), - click the **Edit** button in order to set a new payment method (see [related documentation](/cloud/account/account-billing)). :::note You can attach a dedicated card to your project by choosing the payment method directly from this page. In that way, you can manage your subscriptions with different cards. ::: :::tip In the Usage section of the *Billing & Usage* tab, you can see the current monthly usage of your project compared to the maximum usage allowed by your project's subscription. Use the arrows in the top right corner to see the project's usage for any chosen month. Note also that if your usage indicates that another subscription plan would fit better for your project, a message will be displayed in the *Billing & Usage* tab to advise which plan you could switch to. ::: ### Plans The *Plans* tab displays an overview of the available Strapi Cloud plans and allows you to change your current plan, or your billing cycle. :::info If your current plan is labeled as *legacy*, you will be able to sidegrade to a new plan (see [downgrade section](#downgrading-to-another-plan)). Once you sidegrade, you will no longer have access to your previous plan. ::: #### Upgrading to another plan Plan upgrades are immediate and can be managed, for each project, via the project settings. To upgrade your current plan to a higher one: 1. In the *Plans* tab of your project settings, choose between monthly and yearly billing frequency, and click on the **Upgrade** button of the plan you want to upgrade to. 2. In the window that opens, review the payment details and terms of the upgrade. a. (optional) Click the **Edit** button to select another payment method. b. (optional) Click **I have a discount code**, enter your discount code in the field, and click on the **Apply** button. 3. Click on the **Upgrade to [plan name]** button to confirm the upgrade. The project will automatically be re-deployed. #### Downgrading to another plan Plan downgrades can be managed, for each project, via the project settings. Downgrades are, however, not immediately effective: the current plan will remain active until the end of the current billing period. :::caution Make sure to check the usage of your Strapi Cloud project before downgrading: if your current usage exceeds the limits of the lower plan, you are taking the risk of getting charged for overages. You may also lose access to some features: for example, downgrading to the Essential plan would result in the loss of all your project's backups. Please refer to [Information on billing & usage](/cloud/getting-started/usage-billing) for more information. Note also that you cannot downgrade if you have additional paid environments. You will first need to delete all additional environments that were not included in the base price of your plan (see [Clearing and deleting environments](#resetting--deleting-environment)) before you can schedule a downgrade. When downgrading from Scale to Pro, the additional included environment will automatically be deleted when the downgrade takes effect. You cannot downgrade to the Free plan if a GitHub organization repository is connected to the project. To downgrade, first switch to a personal repository in the [Connected Git repository](#modifying-git-repository--branch) settings. ::: To downgrade your current plan to a lower one: 1. In the *Plans* tab of your project settings, choose between monthly and yearly billing frequency and click on the **Downgrade** button of the plan you want to downgrade to. 2. In the window that opens, review the terms of the downgrade. 3. Click on the **Downgrade** button to confirm the downgrade. The project will automatically be re-deployed. :::tip Downgrades are effective at the end of the current billing period. Whilst the change is pending, you can cancel the scheduled downgrade and stay on your current plan. ::: #### Changing billing cycle You can switch your project's billing cycle between monthly and yearly billing at any time. While project plans and addons can either be billed monthly or yearly depending on your billing cycle, overages are always billed monthly. To change your billing cycle: 1. In the *Plans* tab of your project settings, use the toggle at the top of the plans section to switch between monthly and yearly billing. 2. Click the **Switch to [monthly/yearly] billing** button of your current plan. 3. In the window that opens, review the terms of the billing cycle change. 4. Click **Confirm switch** to confirm the change. :::note When switching from yearly to monthly billing, your plan will remain on its yearly cycle until your next renewal date. Whilst the change is pending, you can cancel the scheduled change and stay on your current billing cycle. When switching from monthly to yearly, however, the change is immediate. ::: ### Invoices The *Invoices* tab displays the full list of invoices for your Strapi Cloud project as well as their status. No invoice is issued for the Free plan. Invoices can have any of the following statuses: - Paid: the payment has been done and the invoice is available, no additional action is required. - Payment pending: the invoice is not complete or validated yet - Payment due: the payment didn't go through and needs to be fixed - Not paid: the payment has failed and won't automatically be retried - Voided: the invoice has been cancelled. :::tip Click the ![download icon](/img/assets/icons/download.svg) icon to download an invoice. ::: :::strapi Invoices are also available in your profile settings. In the *Profile > Invoices* tab, you will find the complete list of invoices for all your projects. Feel free to check the [dedicated documentation](/cloud/account/account-billing#account-invoices). ::: ## Environment-level settings In the project's environments' settings, you first need to select the environment whose settings you would like to configure, using the dropdown. Depending on the chosen environment, there are 3 to 4 tabs available: - [*Configuration*](#configuration), - [*Backups*](#backups), which are only available for the production environment, - [*Domains*](#domains), - and [*Variables*](#variables). ### Configuration The *Configuration* tab for the environment-level settings enables you to check and update the following options for the project: - *Basic information*, to see: - the name of your Strapi Cloud project's environment. The environment name is set when it is created and cannot be modified afterwards. - the Node version of the environment: to change the Node version of the project (see [Modifying Node version](#modifying-node-version)). - the app's internal name for the environment, which can be useful for debug & support purposes. - *Connected branch*: to change the branch of the GitHub repository used for your environment (see [Editing Git branch](#editing-git-branch)). Also allows to enable/disable the "deploy on push" option. - *Environment data*: to transfer data from another environment within the same project (see [Transferring data between environments](#transferring-data-between-environments)) or to remove all data and assets from the current environment while keeping its settings (see [Clearing an environment](#clearing-an-environment)). - *Danger zone*: to permanently delete an additional environment (see [Deleting an environment](#deleting-an-environment)). #### Modifying Node version The environment's Node version is based on the one chosen at the creation of the project (see [Creating a project](/cloud/getting-started/deployment)), through the advanced settings. It is possible to switch to another Node version afterwards, for any environment. 1. In the *Basic information* section of the *Configuration* tab, click on the *Node version*'s edit button. 2. Using the *Node version* drop-down in the dialog, click on the version of your choice. 3. Click on **Save**, or **Save & deploy** if you want the changes to take effect immediately. :::tip Ensure the Node version configured in your Strapi project matches the Node version shown in your project’s dashboard before deploying. ::: #### Editing Git branch 1. In the *Edit branch* dialog, edit the available settings. Note that the branch can be edited for all environments at the same time via the project settings, see [General](#general). | Setting name | Instructions | | --------------- | ------------------------------------------------------------------------ | | Selected branch | (mandatory) Choose a branch from the drop-down list. | | Base directory | Write the path of the base directory in the textbox. | | Deploy the project on every commit pushed to this branch | Tick the box to automatically trigger a new deployment whenever a new commit is pushed to the selected branch. Untick it to disable the option. | 2. Click on the **Save & deploy** button for the changes to take effect. #### Transferring data between environments {#transferring-data-between-environments} The data transfer feature allows you to transfer the entire CMS content (database and assets) from one environment to another within the same Strapi Cloud project. This is useful for testing changes in a secondary environment with up-to-date production data, or for preparing and staging content in a secondary environment before taking it to production. Transferring data between environments currently comes with the following limitations: - You can only transfer toward a secondary environment (not the production environment). - Only project owners can initiate and manage ongoing transfers. - Transfers cannot be initiated on projects that are suspended. :::caution Data transfers are destructive Transferring data to an environment will permanently overwrite all existing data and assets in the target environment. The source environment's data remains unaffected, and its CMS can be accessed during the transfer. Environment settings (such as variables and domains) are not affected by the transfer. ::: To transfer data to a secondary environment: 1. Create and deploy both the source and target [environments](#environments). 2. In the *Environment data* section of the *Configuration* tab, click on the **Import data** button. 3. In the modal that opens, select the source environment from the drop-down list. Only fully created and deployed environments are available as sources. 4. Click on **Import data** to proceed, and follow the steps to confirm the transfer. 5. Once initiated, you will be redirected to the environment's dashboard where you can monitor the transfer's progress. Once the transfer is completed, the dashboard will refresh, showing both the ongoing and historic deployments. :::note The CMS of the target environment will be inaccessible whilst the transfer is ongoing. You can cancel an ongoing transfer, but this will leave the target environment empty. If an error occurs during the transfer, you will have the option to retry or cancel. ::: #### Clearing and deleting environments {#resetting--deleting-environment} You can clear database content and assets from any environment, including the production environment, or permanently delete additional environments. The default production environment cannot be deleted. :::note On the Scale plan, you cannot delete the included secondary environment. You can, however, clear its content. ::: Clearing and deleting are permanent and only available to the project owner. You cannot initiate an environment clearing while the project is suspended. ##### Clearing an environment {#clearing-an-environment} :::warning Environment clearing is destructive Clearing an environment will permanently delete all its existing data and assets. Environment settings (such as variables and domains) are not affected by the clearing. ::: To clear an environment: 1. In the project settings, select the environment then open the *Configuration* tab. 2. In the *Environment data* section, click **Clear environment**. 3. In the confirmation dialog, type the environment name, then confirm to start clearing. You are redirected to the environment dashboard, where you can follow the progress. While clearing is in progress: - The environment CMS is not available. - You cannot trigger a deployment or view the logs for that environment. - Plan upgrade, plan downgrade and setting changes that require a deployment are temporarily unavailable. If clearing fails, click **Retry** on the environment dashboard to run the operation again. ##### Deleting an environment {#deleting-an-environment} 1. In the *Danger zone* section of the *Configuration* tab, click on the **Delete environment** button. 2. Write in the textbox your *Environment name*. 3. Click on the **Delete environment** button to confirm the deletion. ### Backups {#backups} The *Backups* tab informs you of the status and date of the latest backup of your Strapi Cloud projects. The databases associated with all existing Strapi Cloud projects are indeed automatically backed up (weekly for Pro plans and daily for Scale plans). Backups are retained for a 28-day period. Additionally, you can create a single manual backup. :::note Notes - The backup feature is not available for Strapi Cloud projects on the Free or Essential plans. You will need to upgrade to the Pro or Scale plan to enable automatic backups and access the manual backup option. - Backups include only the database of your default Production environment. Assets uploaded to your project and databases from any secondary environments are not included. - The manual backup option becomes available shortly after the project’s first successful deployment. ::: :::tip For projects created before the release of the Backup feature in October 2023, the first backup will automatically be triggered with the next deployment of the project. ::: #### Creating a manual backup To create a manual backup, in the *Backups* section, click on the **Create backup** button. The manual backup should start immediately, and restoration or creation of other backups will be disabled until the backup is complete. :::caution When creating a new manual backup, any existing manual backup will be deleted. You can only have one manual backup at a time. ::: #### Restoring a backup If you need to restore a backup of your project: 1. In the *Backups* section, click on the **Restore backup** button. 2. In the dialog, choose one of the available backups (automatic or manual) of your project in the *Choose backup* drop-down. 3. Click on the **Restore** button of the dialog. Once the restoration is finished, your project will be back to the state it was at the time of the chosen backup. You will be able to see the restoration timestamp and the backup restored in the *Backups* tab. 4. The timestamp of the last completed restoration will be displayed to help you track when the project was last restored. #### Downloading a backup If you need to download a backup of your project: 1. In the *Backups* section, click on the **Download backup** button. 2. In the dialog, choose one of the available backups (automatic or manual) of your project in the *Choose backup* drop-down. 3. Click on the **Download** button of the dialog to download the chosen backup's archive file in `.sql` format. :::note The backup file will include only the database of your default Production environment. It will not include assets or any other environment databases. ::: ### Domains The *Domains* tab enables you to manage domains and connect new ones. All existing domains for your Strapi Cloud project are listed in the *Domains* tab. For each domain, you can: - see its current status: - Active: the domain is currently confirmed and active - Pending: the domain transfer is being processed, waiting for DNS changes to propagate - Failed: the domain change request did not complete as an error occurred - click the edit button to access the settings of the domain - click the delete button to delete the domain #### Connecting a custom domain Default domain names are made of 2 randomly generated words followed by a hash. They can be replaced by any custom domain of your choice. :::note Custom domains are not available on the Free plan. Downgrading to the Free plan will result in the application domain's being restored to the default one. ::: 1. Click the **Connect new domain** button. 2. In the window that opens, fill in the following fields: | Setting name | Instructions | | ------------------------- | ------------------------------------------------------------------------- | | Domain name | Type the new domain name (e.g. *custom-domain-name.com*) | | Hostname | Type the hostname (i.e. address end-users enter in web browser, or call through APIs). | | Target | Type the target (i.e. actual address where users are redirected when entering hostname). | | Set as default domain | Tick the box to make the new domain the default one. | 3. Click on **Save & deploy** for the changes to take effect. :::tip To finish setting up your custom domain, in the settings of your domain registrar or hosting platform, please add the Target value (e.g., `proud-unicorn-123456af.strapiapp.com`) as a CNAME alias to the DNS records of your domain. ::: :::info Custom domains and assets When using custom domains, these domains do not apply to the URLs of uploaded assets. Uploaded assets keep the Strapi Cloud project-based URL. This means that, if your custom domain is hosted at `https://my-custom-domain.com` and your Strapi Cloud project name is `my-strapi-cloud-instance`, API calls will still return URLs such as `https://my-strapi-cloud-instance.media.strapiapp.com/example.png`. Media library queries over REST or GraphQL always return the project media domain on Strapi Cloud. If you move from a self-hosted project, media URLs will no longer match your own domain or CDN. Plan to use the absolute URLs returned by the API, or adjust your frontend to allow the Strapi Cloud media domain (see [Cloud Fundamentals](/cloud/cloud-fundamentals) for more details). ::: ### Variables Environment variables (more information in the [CMS Documentation](/cms/configurations/environment)) are used to configure the environment of your Strapi application, such as the database connection. :::note Custom variables are available to the Strapi server at runtime. Variables prefixed with `STRAPI_ADMIN_` are not exposed to the admin front end on Strapi Cloud. ::: In the *Variables* tab are listed both the default and custom environment variables for your Strapi Cloud project. Each variable is composed of a *Name* and a *Value*. #### Managing environment variables Hovering on an environment variable, either default or custom, displays the following available options: - **Show value** to replace the `*` characters with the actual value of a variable. - **Copy to clipboard** to copy the value of a variable. - **Actions** to access the Edit and Delete buttons. - When editing a default variable, the *Name* cannot be modified and the *Value* can only be automatically generated using the Generate value button. Don't forget to **Save**, or **Save & deploy** if you want the changes to take effect immediately. - When editing a custom variable, both the *Name* and *Value* can be modified by writing something new or by using the Generate value button. Don't forget to **Save**, or **Save & deploy** if you want the changes to take effect immediately. - When deleting a variable, you will be asked to confirm by selecting **Save**, or **Save & deploy** if you want the changes to take effect immediately. :::tip Use the search bar to find more quickly an environment variable in the list! ::: #### Creating custom environment variables Custom environment variables can be created for the Strapi Cloud project. Make sure to redeploy your project after creating or editing an environment variable. 1. In the *Custom environment variables* section, click on the **Add variable** button. 2. Write the *Name* and *Value* of the new environment variable in the same-named fields. Alternatively, you can click on the icon to generate automatically the name and value. 3. (optional) Click on **Add another** to directly create one or more other custom environment variables. 4. Click on the **Save** button to confirm the creation of the custom environment variables. To apply your changes immediately, click on **Save & deploy**. # Admin panel customization Source: https://docs.strapi.io/cms/admin-panel-customization # Admin panel customization The admin panel can be tailored to match your branding, by editing `src/admin/app` and using an `extensions` folder to swap logos, favicon, locales, translations, themes, bundlers, or editors. The **front-end part of Strapi** For a clarification on the distinction between:
  • the Strapi admin panel (front end of Strapi),
  • the Strapi server (back end of Strapi),
  • and the end-user-facing front end of a Strapi-powered application,
refer to the [development introduction](/cms/customization). is called the admin panel. The admin panel presents a graphical user interface to help you structure and manage the content that will be accessible through the Content API. To get an overview of the admin panel, please refer to the [Getting Started > Admin panel](/cms/features/admin-panel) page. From a developer point of view, Strapi's admin panel is a React-based single-page application that encapsulates all the features and installed plugins of a Strapi application. Admin panel customization is done by tweaking the code of the `src/admin/app` file or other files included in the `src/admin` folder (see [project structure](/cms/project-structure)). By doing so, you can: - Customize some parts of the admin panel to better reflect your brand identity (logos, favicon) or your language, - Replace some other parts of the admin panel, such as the Rich text editor and the bundler, - Extend the theme or the admin panel to add new features or customize the existing user interface. :::strapi Plugins and Admin Panel API In addition to supported customizations detailed in this section, you can go further and create plugins that tap into the [Admin Panel API](/cms/plugins-development/admin-panel-api). ::: ## General considerations :::prerequisites Before updating code to customize the admin panel: - Rename the default `app.example.tsx|js` file into `app.ts|js`. - Create a new `extensions` folder in `/src/admin/`. - If you want to see your changes applied live while developing, ensure the admin panel server is running (it's usually done with the `yarn develop` or `npm run develop` command if you have not changed the default [host, port, and path](/cms/configurations/admin-panel#admin-panel-server) of the admin panel). ::: Most basic admin panel customizations will be done in the `/src/admin/app` file, which includes a `config` object. Any file used by the `config` object (e.g., a custom logo) should be placed in a `/src/admin/extensions/` folder and imported inside `/src/admin/app.js`. :::tip Tip: Hot reloading while developing In Strapi 5, the server runs in `watch-admin` mode by default, so the admin panel auto-reloads whenever you change its code. This simplifies admin panel and front-end plugins development. To disable this, run `yarn develop --no-watch-admin` (see [CLI reference](/cms/cli#strapi-develop)). ::: Before deployment, the admin panel needs to be built, by running the following command from the project's root directory: ```sh yarn build ``` ```sh npm run build ``` This will replace the folder's content located at `./build`. Visit [http://localhost:1337/admin](http://localhost:1337/admin) to make sure customizations have been taken into account. :::note Note: Admin panel extensions vs. plugins extensions By default, Strapi projects already contain another `extensions` folder in `/src` but it is for plugins extensions only (see [Plugins extension](/cms/plugins-development/plugins-extension)). ::: ## Available customizations The `config` object of `/src/admin/app` accepts the following parameters: | Parameter | Type | Description | | ------------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------- | | `auth` | Object | Accepts a `logo` key to replace the default Strapi logo on login screen | | `head` | Object | Accepts a `favicon` key to replace the default Strapi favicon | | `locales` | Array of Strings | Defines availables locales | | `translations` | Object | Extends the translations | | `menu` | Object | Accepts the `logo` key to change the logo in the main navigation | | `theme.light` and `theme.dark` | Object | Overwrite theme properties for light and dark modes | | `tutorials` | Boolean | Toggles displaying the video tutorials | `notifications` | Object | Accepts the `releases` key (Boolean) to toggle displaying notifications about new releases |
Click on any of the following cards to get more details about a specific topic: - [Logos](/cms/admin-panel-customization/logos): Update the logos displayed in the admin panel to match your own brand. - [Favicon](/cms/admin-panel-customization/favicon): Update the favicon to match your own brand. - [Locales & translations](/cms/admin-panel-customization/locales-translations): Define locales and extend translations available in the admin panel. - [Rich text editor](/cms/admin-panel-customization/wysiwyg-editor): Learn more about the possible strategies to replace the built-in Rich text editor. - [Bundlers](/cms/admin-panel-customization/bundlers): Choose between the Vite and webpack bundlers and configure them. - [Theme extension](/cms/admin-panel-customization/theme-extension): Learn the basics of extending the built-in theme of the admin panel. - [Admin panel extension](/cms/admin-panel-customization/extension): Learn the basics of extending the admin panel. ## Basic example The following is an example of a basic customization of the admin panel: ```jsx title="/src/admin/app.js" config: { // Replace the Strapi logo in auth (login) views auth: { logo: AuthLogo, }, // Replace the favicon head: { favicon: favicon, }, // Add a new locale, other than 'en' locales: ["fr", "de"], // Replace the Strapi logo in the main navigation menu: { logo: MenuLogo, }, // Override or extend the theme theme: { // overwrite light theme properties light: { colors: { primary100: "#f6ecfc", primary200: "#e0c1f4", primary500: "#ac73e6", primary600: "#9736e8", primary700: "#8312d1", danger700: "#b72b1a", }, }, // overwrite dark theme properties dark: { // ... }, }, // Extend the translations translations: { fr: { "Auth.form.email.label": "test", Users: "Utilisateurs", City: "CITY (FRENCH)", // Customize the label of the Content Manager table. Id: "ID french", }, }, // Disable video tutorials tutorials: false, // Disable notifications about new Strapi releases notifications: { releases: false }, }, bootstrap() {}, }; ``` If TypeScript reports missing module declarations for imported image files, include Vite's client types in `/src/admin/tsconfig.json`: ```json title="/src/admin/tsconfig.json" { "compilerOptions": { "types": ["vite/client"] } } ``` ```ts title="/src/admin/app.ts" config: { // Replace the Strapi logo in auth (login) views auth: { logo: AuthLogo, }, // Replace the favicon head: { favicon: favicon, }, // Add a new locale, other than 'en' locales: ["fr", "de"], // Replace the Strapi logo in the main navigation menu: { logo: MenuLogo, }, // Override or extend the theme theme: { dark:{ colors: { alternative100: '#f6ecfc', alternative200: '#e0c1f4', alternative500: '#ac73e6', alternative600: '#9736e8', alternative700: '#8312d1', buttonNeutral0: '#ffffff', buttonPrimary500: '#7b79ff', // you can see other colors in the link below }, }, light:{ // you can see the light color here just like dark colors https://github.com/strapi/design-system/blob/main/packages/design-system/src/themes/lightTheme/light-colors.ts }, }, }, // Extend the translations // you can see the traslations keys here https://github.com/strapi/strapi/blob/develop/packages/core/admin/admin/src/translations translations: { fr: { "Auth.form.email.label": "test", Users: "Utilisateurs", City: "CITY (FRENCH)", // Customize the label of the Content Manager table. Id: "ID french", }, }, // Disable video tutorials tutorials: false, // Disable notifications about new Strapi releases notifications: { releases: false }, }, bootstrap() {}, }; ``` :::strapi Detailed examples in the codebase * You can see the full translation keys, for instance to change the welcome message, [on GitHub](https://github.com/strapi/strapi/blob/develop/packages/core/admin/admin/src/translations). * Light and dark colors are also found [on GitHub](https://github.com/strapi/design-system/tree/main/packages/design-system/src/themes). ::: # Admin panel bundlers Source: https://docs.strapi.io/cms/admin-panel-customization/bundlers # Admin panel bundlers Supported JavaScript bundlers influence builds and development flow. Strapi's [admin panel](/cms/admin-panel-customization) is a React-based single-page application that encapsulates all the features and installed plugins of a Strapi application. 2 different bundlers can be used with your Strapi 5 application, [Vite](#vite) (the default one) and [webpack](#webpack). Both bundlers can be configured to suit your needs. :::info For simplification, the following documentation mentions the `strapi develop` command, but in practice you will probably use its alias by running either `yarn develop` or `npm run develop` depending on your package manager of choice. ::: ## Vite In Strapi 5, [Vite](https://vitejs.dev/) is the default bundler that Strapi uses to build the admin panel. Vite will therefore be used by default when you run the `strapi develop` command. To extend the usage of Vite, define a function that extends its configuration inside `/src/admin/vite.config`: ```js title="/src/admin/vite.config.js" const { mergeConfig } = require("vite"); module.exports = (config) => { // Important: always return the modified config return mergeConfig(config, { resolve: { alias: { "@": "/src", }, }, }); }; ``` ```ts title="/src/admin/vite.config.ts" // Important: always return the modified config return mergeConfig(config, { resolve: { alias: { "@": "/src", }, }, }); }; ``` :::tip Strapi also supports the `.mts` file extension for Vite config files (`vite.config.mts`), for projects that use explicit ESM module resolution in their `package.json`. This is useful as Vite's CJS Node API is [deprecated since Vite 6](https://v6.vite.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated) and will be removed in a future version. ::: ## Webpack In Strapi 5, the default bundler is Vite. To use [webpack](https://webpack.js.org/) as a bundler you will need to pass it as an option to the `strapi develop` command: ```bash strapi develop --bundler=webpack ``` :::prerequisites If you plan to customize webpack, start from the example file in your project root. Rename: - `webpack.config.example.js` → `webpack.config.js` (JavaScript) - or `webpack.config.example.ts` → `webpack.config.ts` (TypeScript) Strapi will pick up `webpack.config.js` or `webpack.config.ts` automatically when you run `strapi develop --bundler=webpack`. ::: To extend webpack v5, define a function that returns a modified config in `/src/admin/webpack.config.js` or `/src/admin/webpack.config.ts`: ```js title="/src/admin/webpack.config.js" module.exports = (config, webpack) => { // Note: we provide webpack above so you should not `require` it // Perform customizations to webpack config config.plugins.push(new webpack.IgnorePlugin(/\/__tests__\//)); // Important: return the modified config return config; }; ``` ```ts title="/src/admin/webpack.config.ts" // Note: we provide webpack above so you should not `require` it // Perform customizations to webpack config config.plugins.push(new webpack.IgnorePlugin(/\/__tests__\//)); // Important: return the modified config return config; }; ``` # Admin panel extension Source: https://docs.strapi.io/cms/admin-panel-customization/extension # Admin panel extension Strapi's React-based admin panel can be extended locally via `/src/admin/app` for project-specific needs or through plugins for reusable, distributable extensions across multiple Strapi instances. Strapi's [admin panel](/cms/admin-panel-customization) is a React-based single-page application that encapsulates all the features and installed plugins of a Strapi application. If the [customization options](/cms/admin-panel-customization#available-customizations) provided by Strapi are not enough for your use case, you will need to extend Strapi's admin panel. Extending Strapi's admin panel means leveraging its React foundation to adapt and enhance the interface and features according to the specific needs of your project, which might imply creating new components or adding new types of fields. There are 2 use cases where you might want to extend the admin panel: | Approach | Scope | Entry point | Docs | |---|---|---|---| | Local extension | One Strapi project | `/src/admin/app.(js\|ts)` and `/src/admin/extensions/` | [Admin panel customization](/cms/admin-panel-customization) | | Plugin extension | Any project that installs your plugin | `[plugin-name]/admin/src/index.(js\|ts)` | [Admin Panel API overview](/cms/plugins-development/admin-panel-api) | - As a Strapi plugin developer, you want to develop a Strapi plugin that extends the admin panel **everytime it's installed in any Strapi application**. 👉 This can be done by taking advantage of the [Admin Panel API for plugins](/cms/plugins-development/admin-panel-api), which lets you add navigation links and settings sections, inject React components into predefined areas, manage state with Redux, extend the Content Manager's Edit and List views, and more. - As a Strapi developer, you want to develop a unique solution for a Strapi user who only needs to extend a specific instance of a Strapi application. 👉 This can be done by directly updating the `/src/admin/app` file, which can import any file located in `/src/admin/extensions`. :::tip Tip: Hot reloading while developing In Strapi 5, the server runs in `watch-admin` mode by default, so the admin panel auto-reloads whenever you change its code. This simplifies admin panel and front-end plugins development. To disable this, run `yarn develop --no-watch-admin` (see [CLI reference](/cms/cli#strapi-develop)). ::: :::note This section is about the admin panel bundle under `/src/admin`. To change server behaviour for a core plugin (for example upload APIs used while browsing `/admin/plugins/upload`), use `./src/extensions//strapi-server.js|ts` as described in [Plugins extension](/cms/plugins-development/plugins-extension). ::: ## When to consider a plugin instead Starting with a direct customization in `/src/admin/app` is the right default for project-specific needs. Consider moving to a plugin-based approach when one or more of these signals appear: - You are duplicating the same admin customization across several Strapi projects. - You want to version and distribute the extension — either internally or through the [Strapi Marketplace](https://market.strapi.io/). - You need stronger automated testing independent from a single project codebase. - Multiple teams need shared ownership and release management for the same extension. For a full introduction to plugin development, see [Developing Strapi plugins](/cms/plugins-development/developing-plugins). :::strapi Additional resources * If you're looking for ways of replacing the default Rich text editor, refer to the [corresponding page](/cms/admin-panel-customization/wysiwyg-editor). * To understand how plugins integrate with the Strapi admin panel, start with the [Admin Panel API overview](/cms/plugins-development/admin-panel-api). ::: # Favicon Source: https://docs.strapi.io/cms/admin-panel-customization/favicon # Favicon Replace the Strapi admin panel favicon by replacing the `favicon.png` file at the project root or configuring the `strapi::favicon` middleware, then rebuild the app. Strapi's [admin panel](/cms/admin-panel-customization) displays its branding on various places, including the [logo](/cms/admin-panel-customization/logos) and the favicon. Replacing these images allows you to match the interface and application to your identity. There are 2 approaches to replacing the favicon: * Replace the `favicon.png` file at the root of a Strapi project * Edit the [`strapi::favicon` middleware configuration](/cms/configurations/middlewares#favicon) with the following code: ```js title="/config/middlewares.js" // … { name: 'strapi::favicon', config: { path: 'my-custom-favicon.png', }, }, // … ``` Once done, rebuild, launch and revisit your Strapi app by running `yarn build && yarn develop` in the terminal. :::caution Make sure that the cached favicon is cleared. It can be cached in your web browser and also with your domain management tool like Cloudflare's CDN. ::: # Homepage customization Source: https://docs.strapi.io/cms/admin-panel-customization/homepage # Homepage customization The admin panel homepage displays default content and profile widgets and supports custom additions through the `app.widgets.register` API. The Homepage is the landing page of the Strapi admin panel. By default, it provides an overview of your content with 6 default widgets: - _Last edited entries_: Displays recently modified content entries, including their content type, status, and when they were updated. - _Last published entries_: Shows recently published content entries, allowing you to quickly access and manage your published content. - _Profile_: Displays a short summary of your profile, including your name, email address, and role. - _Entries_: Displays the total number of Draft & Published entries. - _Project statistics_: Displays statistics about your entries, content-types, locales, assets, and more. - _Deploy_: Displays a **Deploy Now** button that links to [Strapi Cloud](https://cloud.strapi.io/login) for deploying your project. This widget is only displayed in local development and is hidden when the application runs in production. These default widgets cannot currently be removed, but you can customize the Homepage by creating your own widgets. :::note If you recently created a Strapi project, the Homepage may also display a guided tour above widgets if you haven't skipped it yet (see [Admin Panel](/cms/features/admin-panel) documentation for details). ::: ## Adding custom widgets To add a custom widget, you can: - install a plugin from the [Marketplace](/cms/plugins/installing-plugins-via-marketplace) - or create and register your own widgets The present page will describe how to create and register your widgets. ### Registering custom widgets To register a widget, use `app.widgets.register()`: - in the plugin’s [`register` lifecycle method](/cms/plugins-development/server-lifecycle#register) of the `index` file if you're building a plugin (recommended way), - or in the [application's global `register()` lifecycle method](/cms/configurations/functions#register) if you're adding the widget to just one Strapi application without a plugin. :::info The examples on the present page will cover registering a widget through a plugin. Most of the code should be reusable if you register the widget in the application's global `register()` lifecycle method, except you should not pass the `pluginId` property. ::: ```jsx title="src/plugins/my-plugin/admin/src/index.js" register(app) { // Register the plugin itself app.registerPlugin({ id: pluginId, name: 'My Plugin', }); // Register a widget for the Homepage app.widgets.register({ icon: MyWidgetIcon, title: { id: `${pluginId}.widget.title`, defaultMessage: 'My Widget', }, component: async () => { const component = await import('./components/MyWidget'); return component.default; }, /** * Use this instead if you used a named export for your component */ // component: async () => { // const { Component } = await import('./components/MyWidget'); // return Component; // }, id: 'my-custom-widget', pluginId: pluginId, }); }, bootstrap() {}, // ... }; ``` ```tsx title="src/plugins/my-plugin/admin/src/index.ts" register(app: StrapiApp) { // Register the plugin itself app.registerPlugin({ id: pluginId, name: 'My Plugin', }); // Register a widget for the Homepage app.widgets.register({ icon: MyWidgetIcon, title: { id: `${pluginId}.widget.title`, defaultMessage: 'My Widget', }, component: async () => { const component = await import('./components/MyWidget'); return component.default; }, /** * Use this instead if you used a named export for your component */ // component: async () => { // const { Component } = await import('./components/MyWidget'); // return Component; // }, id: 'my-custom-widget', pluginId: pluginId, }); }, bootstrap() {}, // ... }; ``` :::note The API requires Strapi 5.13+ The `app.widgets.register` API only works with Strapi 5.13 and above. Trying to call the API with older versions of Strapi will crash the admin panel. Plugin developers who want to register widgets should either: - set `^5.13.0` as their `@strapi/strapi` peerDependency in their plugin `package.json`. This peer dependency powers the Marketplace's compatibility check. - or check if the API exists before calling it: ```js if ('widgets' in app) { // proceed with the registration } ``` The peerDependency approach is recommended if the whole purpose of the plugin is to register widgets. The second approach makes more sense if a plugin wants to add a widget but most of its functionality is elsewhere. ::: #### Widget API reference The `app.widgets.register()` method can take either a single widget configuration object or an array of configuration objects. Each widget configuration object can accept the following properties: | Property | Type | Description | Required | |-------------|------------------------|-------------------------------------------------------|----------| | `icon` | `React.ComponentType` | Icon component to display beside the widget title | Yes | | `title` | `MessageDescriptor` | Title for the widget with translation support | Yes | | `component` | `() => Promise` | Async function that returns the widget component | Yes | | `id` | `string` | Unique identifier for the widget | Yes | | `link` | `Object` | Optional link to add to the widget (see link object properties)| No | | `pluginId` | `string` | ID of the plugin registering the widget | No | | `permissions` | `Permission[]` | Permissions required to view the widget | No | **Link object properties:** If you want to add a link to your widget (e.g., to navigate to a detailed view), you can provide a `link` object with the following properties: | Property | Type | Description | Required | |----------|---------------------|------------------------------------------------|----------| | `label` | `MessageDescriptor` | The text to display for the link | Yes | | `href` | `string` | The URL where the link should navigate to | Yes | ### Creating a widget component Widget components should be designed to display content in a compact and informative way. Here's how to implement a basic widget component: ```jsx title="src/plugins/my-plugin/admin/src/components/MyWidget/index.js" const MyWidget = () => { const [loading, setLoading] = useState(true); const [data, setData] = useState(null); const [error, setError] = useState(null); useEffect(() => { // Fetch your data here const fetchData = async () => { try { // Replace with your actual API call const response = await fetch('/my-plugin/data'); const result = await response.json(); setData(result); setLoading(false); } catch (err) { setError(err); setLoading(false); } }; fetchData(); }, []); if (loading) { return ; } if (error) { return ; } if (!data || data.length === 0) { return ; } return (
{/* Your widget content here */}
    {data.map((item) => (
  • {item.name}
  • ))}
); }; ``` ```tsx title="src/plugins/my-plugin/admin/src/components/MyWidget/index.tsx" interface DataItem { id: number; name: string; } const MyWidget: React.FC = () => { const [loading, setLoading] = useState(true); const [data, setData] = useState(null); const [error, setError] = useState(null); useEffect(() => { // Fetch your data here const fetchData = async () => { try { // Replace with your actual API call const response = await fetch('/my-plugin/data'); const result = await response.json(); setData(result); setLoading(false); } catch (err) { setError(err instanceof Error ? err : new Error(String(err))); setLoading(false); } }; fetchData(); }, []); if (loading) { return ; } if (error) { return ; } if (!data || data.length === 0) { return ; } return (
{/* Your widget content here */}
    {data.map((item) => (
  • {item.name}
  • ))}
); }; ``` :::tip For simplicity, the example below uses data fetching directly inside a useEffect hook. While this works for demonstration purposes, it may not reflect best practices in production. For more robust solutions, consider alternative approaches recommended in the [React documentation](https://react.dev/learn/build-a-react-app-from-scratch#data-fetching). If you're looking to integrate a data fetching library, we recommend using [TanStackQuery](https://tanstack.com/query/v3/). ::: **Data management**: ![Rendering and Data management](/img/assets/homepage-customization/rendering-data-management.png) The green box above represents the area where the user’s React component (from `widget.component` in the [API](#widget-api-reference)) is rendered. You can render whatever you like inside of this box. Everything outside that box is, however, rendered by Strapi. This ensures overall design consistency within the admin panel. The `icon`, `title`, and `link` (optional) properties provided in the API are used to display the widget. #### Widget helper components reference Strapi provides several helper components to maintain a consistent user experience across widgets: | Component | Description | Usage | |------------------|-----------------------------------------------------|--------------------------------------| | `Widget.Loading` | Displays a loading spinner and message | When data is being fetched | | `Widget.Error` | Displays an error state | When an error occurs | | `Widget.NoData` | Displays when no data is available | When the widget has no data to show | | `Widget.NoPermissions` | Displays when user lacks required permissions | When the user cannot access the widget | These components help maintain a consistent look and feel across different widgets. You could render these components without children to get the default wording: `` or you could pass children to override the default copy and specify your own wording: `Your custom error message`. ## Example: Adding a content metrics widget The following is a complete example of how to create a content metrics widget that displays the number of entries for each content type in your Strapi application. The end result will look like the following in your admin panel's Homepage: The widget shows counts for example content-types automatically generated by Strapi when you provide the `--example` flag on installation (see [CLI installation options](/cms/installation/cli#cli-installation-options) for details). This widget can be added to Strapi by: 1. creating a "content-metrics" plugin (see [plugin creation](/cms/plugins-development/create-a-plugin) documentation for details) 2. re-using the code examples provided below. :::tip If you prefer a hands-on approach, you can reuse the following [CodeSandbox link](https://codesandbox.io/p/sandbox/github/pwizla/strapi-custom-widget-content-metrics). ::: The following file registers the plugin and the widget: ```jsx title="src/plugins/content-metrics/admin/src/index.js" {28-42} register(app) { app.addMenuLink({ to: `plugins/${PLUGIN_ID}`, icon: PluginIcon, intlLabel: { id: `${PLUGIN_ID}.plugin.name`, defaultMessage: PLUGIN_ID, }, Component: () => import('./pages/App'), }); app.registerPlugin({ id: PLUGIN_ID, initializer: Initializer, isReady: false, name: PLUGIN_ID, }); // Registers the widget app.widgets.register({ icon: Stethoscope, title: { id: `${PLUGIN_ID}.widget.metrics.title`, defaultMessage: 'Content Metrics', }, component: async () => { const component = await import('./components/MetricsWidget'); return component.default; }, id: 'content-metrics', pluginId: PLUGIN_ID, }); }, async registerTrads({ locales }) { return Promise.all( locales.map(async (locale) => { try { const { default: data } = await import(`./translations/${locale}.json`); return { data, locale }; } catch { return { data: {}, locale }; } }) ); }, bootstrap() {}, }; ``` The following file defines the widget's component and its logic. It's tapping into a specific controller and route that we'll create for the plugin: ```jsx title="src/plugins/content-metrics/admin/src/components/MetricsWidget/index.js" const MetricsWidget = () => { const [loading, setLoading] = useState(true); const [metrics, setMetrics] = useState(null); const [error, setError] = useState(null); useEffect(() => { const fetchMetrics = async () => { try { const response = await fetch('/api/content-metrics/count'); const data = await response.json(); console.log("data:", data); const formattedData = {}; if (data && typeof data === 'object') { Object.keys(data).forEach(key => { const value = data[key]; formattedData[key] = typeof value === 'number' ? value : String(value); }); } setMetrics(formattedData); setLoading(false); } catch (err) { console.error(err); setError(err.message || 'An error occurred'); setLoading(false); } }; fetchMetrics(); }, []); if (loading) { return ( ); } if (error) { return ( ); } if (!metrics || Object.keys(metrics).length === 0) { return No content types found; } return ( {Object.entries(metrics).map(([contentType, count], index) => ( {String(contentType)} {String(count)} ))} ); }; ``` The following file defines a custom controller that counts all content-types: ```js title="src/plugins/content-metrics/server/src/controllers/metrics.js" 'use strict'; module.exports = ({ strapi }) => ({ async getContentCounts(ctx) { try { // Get all content types const contentTypes = Object.keys(strapi.contentTypes) .filter(uid => uid.startsWith('api::')) .reduce((acc, uid) => { const contentType = strapi.contentTypes[uid]; acc[contentType.info.displayName || uid] = 0; return acc; }, {}); // Count entities for each content type for (const [name, _] of Object.entries(contentTypes)) { const uid = Object.keys(strapi.contentTypes) .find(key => strapi.contentTypes[key].info.displayName === name || key === name ); if (uid) { // Using the count() method from the Document Service API const count = await strapi.documents(uid).count(); contentTypes[name] = count; } } ctx.body = contentTypes; } catch (err) { ctx.throw(500, err); } } }); ``` The following file ensures that the metrics controller is reachable at a custom `/count` route: ```js title="src/plugins/content-metrics/server/src/routes/index.js" 'content-api': { type: 'content-api', routes: [ { method: 'GET', path: '/count', handler: 'metrics.getContentCounts', config: { policies: [], }, }, ], }, }; ``` The following file registers the plugin and the widget: ```tsx title="src/plugins/content-metrics/admin/src/index.ts" {28-42} register(app) { app.addMenuLink({ to: `plugins/${PLUGIN_ID}`, icon: PluginIcon, intlLabel: { id: `${PLUGIN_ID}.plugin.name`, defaultMessage: PLUGIN_ID, }, Component: () => import('./pages/App'), }); app.registerPlugin({ id: PLUGIN_ID, initializer: Initializer, isReady: false, name: PLUGIN_ID, }); // Registers the widget app.widgets.register({ icon: Stethoscope, title: { id: `${PLUGIN_ID}.widget.metrics.title`, defaultMessage: 'Content Metrics', }, component: async () => { const component = await import('./components/MetricsWidget'); return component.default; }, id: 'content-metrics', pluginId: PLUGIN_ID, }); }, async registerTrads({ locales }) { return Promise.all( locales.map(async (locale) => { try { const { default: data } = await import(`./translations/${locale}.json`); return { data, locale }; } catch { return { data: {}, locale }; } }) ); }, bootstrap() {}, }; ``` The following file defines the widget's component and its logic. It's tapping into a specific controller and route that we'll create for the plugin: ```tsx title="src/plugins/content-metrics/admin/src/components/MetricsWidget/index.ts" const MetricsWidget = () => { const [loading, setLoading] = useState(true); const [metrics, setMetrics] = useState(null); const [error, setError] = useState(null); useEffect(() => { const fetchMetrics = async () => { try { const response = await fetch('/api/content-metrics/count'); const data = await response.json(); console.log("data:", data); const formattedData = {}; if (data && typeof data === 'object') { Object.keys(data).forEach(key => { const value = data[key]; formattedData[key] = typeof value === 'number' ? value : String(value); }); } setMetrics(formattedData); setLoading(false); } catch (err) { console.error(err); setError(err.message || 'An error occurred'); setLoading(false); } }; fetchMetrics(); }, []); if (loading) { return ( ); } if (error) { return ( ); } if (!metrics || Object.keys(metrics).length === 0) { return No content types found; } return ( {Object.entries(metrics).map(([contentType, count], index) => ( {String(contentType)} {String(count)} ))} ); }; ``` The following file defines a custom controller that counts all content-types: ```js title="src/plugins/content-metrics/server/src/controllers/metrics.js" 'use strict'; module.exports = ({ strapi }) => ({ async getContentCounts(ctx) { try { // Get all content types const contentTypes = Object.keys(strapi.contentTypes) .filter(uid => uid.startsWith('api::')) .reduce((acc, uid) => { const contentType = strapi.contentTypes[uid]; acc[contentType.info.displayName || uid] = 0; return acc; }, {}); // Count entities for each content type using Document Service for (const [name, _] of Object.entries(contentTypes)) { const uid = Object.keys(strapi.contentTypes) .find(key => strapi.contentTypes[key].info.displayName === name || key === name ); if (uid) { // Using the count() method from Document Service instead of strapi.db.query const count = await strapi.documents(uid).count(); contentTypes[name] = count; } } ctx.body = contentTypes; } catch (err) { ctx.throw(500, err); } } }); ``` The following file ensures that the metrics controller is reachable at a custom `/count` route: ```js title="src/plugins/content-metrics/server/src/routes/index.js" 'content-api': { type: 'content-api', routes: [ { method: 'GET', path: '/count', handler: 'metrics.getContentCounts', config: { policies: [], }, }, ], }, }; ``` # Admin panel customization - URL, host, and path configuration Source: https://docs.strapi.io/cms/admin-panel-customization/host-port-path # Admin panel customization: Host, port, and path configuration Configure the Strapi admin panel's host, port, and URL path in the `config/admin.[ts|js]` file to change where it is accessible from its default location at `/admin`. By default, Strapi's [admin panel](/cms/admin-panel-customization) is exposed via [http://localhost:1337/admin](http://localhost:1337/admin). For security reasons, the host, port, and path can be updated. ## Update the admin panel's path only Unless you chose to deploy Strapi's back-end server and admin panel server on different servers (see [deployment](/cms/configurations/admin-panel#deploy-on-different-servers)), by default: - The back-end server and the admin panel server of Strapi both run on the same host and port, which is `http://localhost:1337/`. - The admin panel is accessible at the `/admin` path while the back-end server is accessible at the `/api` path. To make the admin panel accessible at another path, for instance at `http://localhost:1337/dashboard`, define or update the `url` property in the [admin panel configuration file](/cms/configurations/admin-panel) as follows: ```js title="/config/admin.js" module.exports = ({ env }) => ({ // … other configuration properties url: "/dashboard", }); ``` Since by default the back-end server and the admin panel server run on the same host and port, only updating the `config/admin.[ts|js]` file should work if you left the `host` and `port` property values untouched in the [server configuration](/cms/configurations/server) file, which should be as follows: ```js title="/config/server.js" module.exports = ({ env }) => ({ host: env("HOST", "0.0.0.0"), port: env.int("PORT", 1337), }); ``` ```js title="/config/server.ts" host: env("HOST", "0.0.0.0"), port: env.int("PORT", 1337), }); ``` ## Update the admin panel's host and port If the admin panel and the back-end server of Strapi are not hosted on the same server (see [deployment](/cms/configurations/admin-panel#deploy-on-different-servers)), you will need to update the host and port of the admin panel. This is done in the admin panel configuration file, for example to host the admin panel on `my-host.com:3000` properties should be updated follows: ```js title="./config/admin.js" module.exports = ({ env }) => ({ host: "my-host.com", port: 3000, // Additionally you can define another path instead of the default /admin one 👇 // url: '/dashboard' }); ``` ```js title="./config/admin.ts" host: "my-host.com", port: 3000, // Additionally you can define another path instead of the default /admin one 👇 // url: '/dashboard' }); ```
:::strapi Other admin panel configurations The `/config/admin.[ts|js]` file can be used to configure many other aspects. Please refer to the [admin panel configuration](/cms/configurations/admin-panel) documentation for details. ::: :::tip Behind a reverse proxy When serving the admin from a different public origin (domain, host, or port) than the API, set `host`/`port` accordingly and ensure your proxy forwards headers (e.g., `X-Forwarded-*`). If the admin is reachable at a different origin, prefer configuring that origin explicitly. ::: # Locales & translations Source: https://docs.strapi.io/cms/admin-panel-customization/locales-translations # Locales & translations Configure the admin panel languages by updating the `config.locales` array and override default or plugin strings with `config.translations` or custom translation files. The Strapi [admin panel](/cms/admin-panel-customization) ships with English strings and supports adding other locales so your editorial team can work in their preferred language. Locales determine which languages appear in the interface, while translations provide the text displayed for each key in a locale. This guide targets project maintainers customizing the admin experience from the application codebase. All examples modify the configuration exported from `/src/admin/app` file, which Strapi loads when the admin panel builds. You'll learn how to declare additional locales and how to extend Strapi or plugin translations when a locale is missing strings. ## Defining locales To update the list of available locales in the admin panel, set the `config.locales` array in `src/admin/app` file: ```js title="/src/admin/app.js" config: { locales: ["ru", "zh"], }, bootstrap() {}, }; ``` ```ts title="/src/admin/app.ts" config: { locales: ["ru", "zh"], }, bootstrap() {}, }; ``` :::note Notes - The `en` locale cannot be removed from the build as it is both the fallback (i.e. if a translation is not found in a locale, the `en` will be used) and the default locale (i.e. used when a user opens the administration panel for the first time). - The full list of available locales is accessible on [Strapi](https://github.com/strapi/strapi/blob/v4.0.0/packages/plugins/i18n/server/constants/iso-locales.json). ::: ## Extending translations Translation key/value pairs are declared in `@strapi/admin/admin/src/translations/[language-name].json` files. These keys can be extended through the `config.translations` key in `src/admin/app` file: ```js title="/src/admin/app.js" config: { locales: ["fr"], translations: { fr: { "Auth.form.email.label": "test", Users: "Utilisateurs", City: "CITY (FRENCH)", // Customize the label of the Content Manager table. Id: "ID french", }, }, }, bootstrap() {}, }; ``` ```ts title="/src/admin/app.ts" config: { locales: ["fr"], translations: { fr: { "Auth.form.email.label": "test", Users: "Utilisateurs", City: "CITY (FRENCH)", // Customize the label of the Content Manager table. Id: "ID french", }, }, }, bootstrap() {}, }; ``` A plugin's key/value pairs are declared independently in the plugin's files at `/admin/src/translations/[language-name].json`. These key/value pairs can similarly be extended in the `config.translations` key by prefixing the key with the plugin's name (i.e. `[plugin name].[key]: 'value'`) as in the following example: ```js title="/src/admin/app.js" config: { locales: ["fr"], translations: { fr: { "Auth.form.email.label": "test", // Translate a plugin's key/value pair by adding the plugin's name as a prefix // In this case, we translate the "plugin.name" key of plugin "content-type-builder" "content-type-builder.plugin.name": "Constructeur de Type-Contenu", }, }, }, bootstrap() {}, }; ``` ```ts title="/src/admin/app.ts" config: { locales: ["fr"], translations: { fr: { "Auth.form.email.label": "test", // Translate a plugin's key/value pair by adding the plugin's name as a prefix // In this case, we translate the "plugin.name" key of plugin "content-type-builder" "content-type-builder.plugin.name": "Constructeur de Type-Contenu", }, }, }, bootstrap() {}, }; ``` If you need to ship additional translation JSON files—for example to organize large overrides or to support a locale not bundled with Strapi—place them in the `/src/admin/extensions/translations` folder and ensure the locale code is listed in `config.locales`. :::tip Rebuild the admin Translation changes apply when the admin rebuilds. If updates don’t show, re-run your dev server or rebuild the admin to refresh bundled translations. ::: # Logos Source: https://docs.strapi.io/cms/admin-panel-customization/logos # Logos Update login and navigation logos by extending the admin app. Prefer SVG for crisp rendering; provide light/dark variants when possible for contrast. Strapi's [admin panel](/cms/admin-panel-customization) displays its branding on both the login screen and in the main navigation. Replacing these images allows you to match the interface to your identity. The present page shows how to override the two logo files via the admin panel configuration. If you prefer uploading them directly in the UI, see [Customizing the logo](/cms/features/admin-panel#customizing-the-logo). The Strapi admin panel displays a logo in 2 different locations, represented by 2 different keys in the admin panel configuration: | Location in the UI | Configuration key to update | | ---------------------- | --------------------------- | | On the login page | `config.auth.logo` | | In the main navigation | `config.menu.logo` | :::note Logos uploaded via the admin panel supersede any logo set through the configuration files. ::: ### Logos location in the admin panel The logo handled by `config.auth.logo` logo is only shown on the login screen: ![Location of the auth logo](/img/assets/development/config-auth-logo.png) The logo handled by `config.menu.logo` logo is located in the main navigation at the top left corner of the admin panel: ![Location of Menu logo](/img/assets/development/config-menu-logo.png) ### Updating logos To update the logos, put image files in the `/src/admin/extensions` folder, import these files in `src/admin/app` and update the corresponding keys as in the following example: ```jsx title="/src/admin/app.js" config: { // … other configuration properties auth: { // Replace the Strapi logo in auth (login) views logo: AuthLogo, }, menu: { // Replace the Strapi logo in the main navigation logo: MenuLogo, }, // … other configuration properties bootstrap() {}, }; ``` ```jsx title="/src/admin/app.ts" config: { // … other configuration properties auth: { // Replace the Strapi logo in auth (login) views logo: AuthLogo, }, menu: { // Replace the Strapi logo in the main navigation logo: MenuLogo, }, // … other configuration properties bootstrap() {}, }; ``` :::note There is no size limit for image files set through the configuration files. ::: # Theme extension Source: https://docs.strapi.io/cms/admin-panel-customization/theme-extension # Theme extension Extend the Strapi admin panel theme for light and dark modes by customizing `config.theme.light` and `config.theme.dark` keys in `/src/admin/app.js` to override colors and other design system properties. Strapi's [admin panel](/cms/admin-panel-customization) can be displayed either in light or dark mode (see [profile setup](/cms/getting-started/setting-up-admin-panel#setting-up-your-administrator-profile)), and both can be extended through custom theme settings. To extend the theme, use either: - the `config.theme.light` key for the Light mode - the `config.theme.dark` key for the Dark mode :::strapi Strapi Design System The default [Strapi theme](https://github.com/strapi/design-system/tree/main/packages/design-system/src/themes) defines various theme-related keys (shadows, colors…) that can be updated through the `config.theme.light` and `config.theme.dark` keys in `./admin/src/app.js`. The [Strapi Design System](https://design-system.strapi.io/) is fully customizable and has a dedicated [StoryBook](https://design-system-git-main-strapijs.vercel.app) documentation. ::: The following example shows how to override the primary color by customizing the light and dark theme keys in the [admin panel configuration](/cms/configurations/admin-panel): ```js title="/src/admin/app.js" config: { theme: { light: { colors: { primary600: "#4A6EFF", }, }, dark: { colors: { primary600: "#9DB2FF", }, }, }, }, bootstrap() {}, } ``` ```ts title="/src/admin/app.ts" config: { theme: { light: { colors: { primary600: '#4A6EFF', }, }, dark: { colors: { primary600: '#9DB2FF', }, }, }, }, bootstrap() {}, } ``` # Customizing the rich text editor Source: https://docs.strapi.io/cms/admin-panel-customization/wysiwyg-editor # Change the default rich text editor Strapi's admin panel includes a built-in WYSIWYG markdown editor for `richtext` fields. You can replace it by installing third-party editor plugins from the Marketplace or creating a custom field for deeper integration. :::note This page covers customization of the **WYSIWYG markdown editor** used for `richtext` fields. For the **Blocks** field (the JSON-based rich text editor), see [Content Manager APIs: addRichTextBlocks](/cms/plugins-development/content-manager-apis#addrichtextblocks). ::: Strapi's [admin panel](/cms/admin-panel-customization) comes with a built-in rich text editor. To change the default editor, several options are at your disposal: - You can install a third-party plugin, such as one for CKEditor, by visiting [Strapi](https://market.strapi.io/). - You can create your own plugin to create and register a fully custom WYSIWYG field (see [custom fields documentation](/cms/features/custom-fields)). :::tip Next steps When evaluating editors, start with a plugin from the Marketplace for a quick trial, then consider a custom field if you need deeper integration (schema, validation, or custom toolbar behavior). ::: # Docs MCP server Source: https://docs.strapi.io/cms/ai/docs-mcp-server # Docs MCP server A Docs MCP server exposes the Strapi documentation to AI coding tools. Connect it to your IDE to get Strapi-aware code suggestions and answers directly in your development environment. The Docs [MCP](https://modelcontextprotocol.io) (Model Context Protocol) server is powered by [Kapa](https://kapa.ai), the same service behind the **Ask AI** button on the documentation website. It draws from the full Strapi documentation, including guides, API references, and code examples. The Docs MCP server is part of the [AI tools for developers](/cms/ai/for-developers) that Strapi offers. :::strapi MCP servers for Strapi Strapi offers 2 different MCP servers: - the Docs MCP server, covered on the present page, - and the Strapi MCP server for content management, covered on its [dedicated feature page](/cms/features/strapi-mcp-server). ::: ## Compatible tools The MCP server works with any tool that supports the MCP protocol, including: - [Cursor](https://cursor.com) - [VS Code](https://code.visualstudio.com) with GitHub Copilot - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) - [Windsurf](https://codeium.com/windsurf) - Any other MCP-compatible IDE or tool ## Connection details When opening the Ask AI window, you should see a **Use MCP** dropdown in the top right corner. Click on it and choose which tool you'd like to connect: If manual MCP server configuration is required: 1. Click the **Copy MCP URL** from the dropdown. The server URL should be: `https://strapi-docs.mcp.kapa.ai` 2. Add the server to your IDE's MCP configuration file: Add to your `.cursor/mcp.json` file: ```json title=".cursor/mcp.json" { "mcpServers": { "strapi-docs": { "url": "https://strapi-docs.mcp.kapa.ai" } } } ``` Add to your `.vscode/mcp.json` file: ```json title=".vscode/mcp.json" { "servers": { "strapi-docs": { "type": "http", "url": "https://strapi-docs.mcp.kapa.ai" } } } ``` Add to your `~/.codeium/windsurf/mcp_config.json` file: ```json title="~/.codeium/windsurf/mcp_config.json" { "mcpServers": { "strapi-docs": { "serverUrl": "https://strapi-docs.mcp.kapa.ai" } } } ``` Once connected, your AI coding assistant can query the Strapi documentation directly to answer questions, suggest implementations, and verify API usage. :::tip For docs-related questions, start your prompts with `Use the strapi-docs MCP server to answer:`. This will ensure the tool queries docs.strapi.io instead of returning answers based on its training data, which can be outdated. ::: # AI for content managers Source: https://docs.strapi.io/cms/ai/for-content-managers # AI for content managers Strapi AI helps content managers design content structures, translate content, and generate asset metadata from the admin panel. Strapi also includes a built-in MCP server that lets AI clients manage content through natural language. This page covers the AI-powered capabilities available to content managers in Strapi: the Strapi AI features built into the admin panel, and the MCP server that lets AI clients manage your content. ## Strapi AI Some Strapi CMS features can be enhanced with Strapi AI, helping content managers and administrators design content structures, translate content automatically, and generate asset metadata, all from the admin panel. ### Activation and configuration {#activation} Strapi AI is available for Growth plan users since Strapi 5.30 and works with both Strapi Cloud and self-hosted deployments. To get started: 1. Upgrade to Strapi v5.30+. AI features are not available on earlier versions. 2. Activate a Growth license key, or start a 30-day free trial via CLI or Strapi Cloud. The trial includes 10 free credits to explore AI features. 3. Access AI features from the Content-Type Builder, Media Library, or Content Manager; they are enabled by default. All Strapi AI features can be enabled or disabled globally through the admin panel configuration: ```js title="/config/admin.js|ts" module.exports = { // ... ai: { enabled: true, // set to false to disable all Strapi AI features }, }; ``` See [Admin panel configuration > Strapi AI](/cms/configurations/admin-panel#strapi-ai) for all configuration options. ### Available features {#features} | Feature | Description | |---------|-------------| | [Content-Type Builder](/cms/features/content-type-builder#strapi-ai) | AI chat assistant that helps design content-type structures, explain existing schemas, and plan data models. Uses your existing content types as context. | | [Internationalization](/cms/features/internationalization#ai-powered-internationalization) | Automatically translates content from the default locale to all other configured locales when you save an entry. | | [Media Library](/cms/features/media-library#ai-powered-metadata-generation) | Generates alternative text, captions, and descriptions for uploaded images. | ### Credits and data handling {#credits} Strapi AI features consume AI credits. Strapi AI includes 1,000 credits per month on the plan, and 10 free credits during the free trial. Strapi AI is not available on Enterprise plans. Lightweight actions use fewer credits, while more complex ones use more. You can check your credit usage in the [Settings Overview](http://localhost:1337/admin/settings/application-infos) of the admin panel. Notifications are sent when your usage reaches 80%, 90%, and 100% of your monthly allowance. Overages apply. Credits are shared across all users within the same project instance. When your credits run out, you can keep using Strapi AI, with overages billed monthly. For more information about Strapi AI, please refer to the [dedicated support article](https://support.strapi.io/articles/1821143913-understanding-strapi-ai). All AI requests are processed through Strapi-managed infrastructure. Content is only used temporarily during each request and is not stored outside your instance. Strapi AI follows the same GDPR-aligned framework as Strapi Cloud. See [Usage information > Strapi AI data handling](/cms/usage-information#strapi-ai-data-handling) for more details. ## Strapi MCP server Strapi includes a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that lets AI clients like Claude, Cursor, or any MCP-compatible tool manage your content through natural language. Once enabled and connected, an AI client can create, read, update, delete, publish, and unpublish entries directly through Strapi's Content Manager, all gated by Admin token permissions. - [MCP server](/cms/features/strapi-mcp-server): Learn how to enable, configure, connect to, and use Strapi # AI for developers Source: https://docs.strapi.io/cms/ai/for-developers # AI for developers The Strapi documentation site includes free AI-powered tools including an AI toolbar, chatbot powered by Kapa, `llms.txt` files, and MCP servers to help developers learn and integrate Strapi more effectively. The Strapi documentation site includes AI-powered tools to help developers learn, explore, and integrate Strapi more effectively. These tools are free to use and available to everyone. :::tip AGENTS.MD files In addition to docs and product features described on the present page, the [strapi/strapi](https://github.com/strapi/strapi/blob/develop/AGENTS.md) and [strapi/documentation](https://github.com/strapi/documentation/blob/main/AGENTS.md) repositories also have their own `AGENTS.md` files. Use them to guide your AI-based tools when developing Strapi features or updating documentation. ::: ## AI toolbar Every documentation page includes an AI toolbar near the top of the page, right after the title. The toolbar provides quick access to all AI-related actions for the current page. Clicking the dropdown arrow reveals additional options: The toolbar includes the following actions: | Action | Description | |--------|-------------| | **Copy Markdown** | Copies the clean Markdown version of the current page to your clipboard. Available in Elegant and AI modes; in Markdown mode, use the **View this page as .md** button next to the toolbar instead. | | **View as Markdown** | Opens the clean Markdown version of the current page in a new tab.
Available in Elegant and AI modes; in Markdown mode, it is replaced by the " View this page as .md" button shown next to the toolbar. | | **Open with ChatGPT** | Opens a new ChatGPT conversation prefilled with the current page URL | | **Open with Claude** | Opens a new Claude conversation and copies the prompt to your clipboard | | **View LLMs.txt** | Opens the lightweight page index for AI models | | **View LLMs-code.txt** | Opens the code examples file for AI models | | **View LLMs-full.txt** | Opens the complete documentation file for AI models | ### Copy Markdown The primary action in the toolbar. Clicking **Copy Markdown** fetches the clean Markdown version of the current page (the same content as the page's `.md` URL, with layout components resolved into plain Markdown) and copies it to your clipboard. You can then paste it into any AI assistant (ChatGPT, Claude, Gemini, etc.) for: - Asking questions about a specific page with full context - Summarizing or simplifying documentation content - Generating code based on documented APIs - Translating documentation into another language In Markdown mode, the toolbar's **Copy Markdown** and **View as Markdown** actions are replaced by a single **View this page as .md** button shown next to the toolbar, which opens the same clean Markdown. You can also reach it directly by adding `.md` to any page URL. ### Open with LLM The **Open with ChatGPT** and **Open with Claude** buttons open a new conversation in the respective AI assistant, prefilled with a prompt that includes the current page URL. The prompt is automatically localized to your browser's language. For Claude, the prompt is also copied to your clipboard since URL encoding works differently. ## AI chatbot {#chatbot} An AI chatbot powered by [Kapa](https://kapa.ai) is integrated directly into the documentation site. It draws from the full documentation, community forums, blog posts, and other Strapi resources to provide contextual answers. ### Sidebar entry point Click the **Ask AI** button in the left sidebar (next to the search bar) to open a conversation about anything related to Strapi. You can ask questions like: - "How do I create a Strapi project?" - "How does population work in REST API?" - "How do I customize the admin panel?" For complex questions, enable **deep thinking mode** for more thorough (but slower) answers. ### Code block entry point Hover over any code block on a documentation page to reveal an **Ask AI** button in the top-right corner of the block. Clicking it opens a conversation prefilled with the code snippet, so you can ask for an explanation or adaptation. This is particularly useful for understanding configuration examples, API responses, or lifecycle hook patterns. ### AI mode entry point Every documentation page can be switched to **AI mode** using the mode selector at the top of the page (next to **Elegant mode** and **Markdown mode**). AI mode splits the page into two columns: the documentation content on the left, and an AI assistant panel on the right. The panel shows an AI-generated summary of the current page and a question box, so you can read the page and ask questions about it side by side, without leaving the page or opening a separate window. Questions are answered with the same Kapa-powered chatbot, scoped to the page you are reading. To leave AI mode, switch back to **Elegant mode** or **Markdown mode** with the mode selector, or click the in the upper-right corner of the AI panel. ## LLMs text files {#llms-txt} 3 text files are available for feeding Strapi documentation content directly to LLMs. These follow the [llms.txt](https://llmstxt.org/) convention and are designed for programmatic consumption by AI tools. | File | URL | Content | Best for | |------|-----|---------|----------| | `llms.txt` | [/llms.txt](https://docs.strapi.io/llms.txt) | Concise, link-rich overview of all pages | High-level context, navigation, RAG pipelines | | `llms-full.txt` | [/llms-full.txt](https://docs.strapi.io/llms-full.txt) | Entire documentation in a single file | Full-site context when token limits allow | | `llms-code.txt` | [/llms-code.txt](https://docs.strapi.io/llms-code.txt) | All code examples, grouped by page | Code-centric work, migrations, API discovery | ### When to use each file - **`llms.txt`**: Use this to give an AI model an overview of what Strapi documentation covers, without consuming too many tokens. Ideal for RAG (Retrieval-Augmented Generation) systems or as a first pass before diving deeper. - **`llms-full.txt`**: Use this when you need the AI to have access to the complete documentation content. This is a large file; make sure your model's context window can handle it. - **`llms-code.txt`**: Use this when you're working on code and want to give an AI all of Strapi's documented code examples. Each snippet includes the source page URL and anchor for traceability. ## MCP servers {#mcp} The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open standard that lets AI tools interact with external services. 2 MCP servers are available for Strapi: - [Strapi MCP server](/cms/features/strapi-mcp-server): Connect AI clients to your Strapi instance to manage content through natural language. - [Docs MCP server](/cms/ai/docs-mcp-server): Connect the Strapi documentation to your IDE for up-to-date, reliable information. ## Tips for better results {#tips} The following tips will help you fine-tune your prompts to get the best results: - Use the [Docs MCP server](/cms/ai/docs-mcp-server) in your IDE for the fastest developer experience. For docs-related questions, prefix your prompt with `Use the strapi-docs MCP server to answer:` so the tool queries docs.strapi.io instead of using potentially outdated training data. - Include the page URL so the assistant grounds its answer in the right context. - Mention your Strapi version (e.g., Strapi 5) to avoid outdated suggestions. - Pair code examples with their source page when sharing snippets from `llms-code.txt`. - Prefer documented APIs over private internals when asking for code generation. # Strapi Client Source: https://docs.strapi.io/cms/api/client # Strapi Client The Strapi Client is a JavaScript library that simplifies interactions with your Strapi back end for fetching, creating, updating, and deleting content through `collection()`, `single()`, and `files()` methods. The Strapi Client library simplifies interactions with your Strapi back end, providing a way to fetch, create, update, and delete content. This guide walks you through setting up the Strapi Client, configuring authentication, and using its key features effectively. ## Getting Started :::prerequisites - A Strapi project has been created and is running. If you haven't set one up yet, follow the [Quick Start Guide](/cms/quick-start) to create one. - You know the URL of the Content API of your Strapi instance (e.g., `http://localhost:1337/api`). ::: ### Installation To use the Strapi Client in your project, install it as a dependency using your preferred package manager: ```bash yarn add @strapi/client ``` ```bash npm install @strapi/client ``` ```bash pnpm add @strapi/client ``` ### Basic configuration To start interacting with your Strapi back end, initialize the Strapi Client and set the base API URL: With Javascript, import the `strapi` function and create a client instance: ```js const client = strapi({ baseURL: 'http://localhost:1337/api' }); ``` With Typescript, import the `strapi` function and create a client instance with your Strapi API base URL: ```typescript const client = strapi({ baseURL: 'http://localhost:1337/api' }); ``` If you're using the Strapi Client in a browser environment, you can include it using a ` ``` The `baseURL` must include the protocol (`http` or `https`). An invalid URL will throw an error `StrapiInitializationError`. ### Authentication The Strapi Client supports different authentication strategies to access protected resources in your Strapi back end. If your Strapi instance uses [API tokens](/cms/features/api-tokens), configure the Strapi Client as follows: ```js const client = strapi({ baseURL: 'http://localhost:1337/api', auth: 'your-api-token-here', }); ``` This allows your requests to include the necessary authentication credentials automatically. If the token is invalid or missing, the client will throw an error during initialization `StrapiValidationError`. ## API Reference The Strapi Client provides the following key properties and methods for interacting with your Strapi back end: | Parameter | Description | | ----------| -------------------------------------------------------------------------------------------- | | `baseURL` | The base API URL of your Strapi back end. | | `fetch()` | A utility method for making generic API requests similar to the native fetch API. | | `collection()` | Manages collection-type resources (e.g., blog posts, products). | | `single()` | Manages single-type resources (e.g., homepage settings, global configurations). | | `files()` | Enables upload, retrieve and management of files directly to/from the Strapi Media Library. | ### General purpose fetch The Strapi Client provides access to the underlying JavaScript `fetch` function to make direct API requests. The request is always relative to the base URL provided during client initialization: ```js const result = await client.fetch('articles', { method: 'GET' }); ``` ### Working with collection types Collection types in Strapi are entities with multiple entries (e.g., a blog with many posts). The Strapi Client provides a `collection()` method to interact with these resources, with the following methods available: | Parameter | Description | | ----------| -------------------------------------------------------------------------------------------- | | `find(queryParams?)` | Fetch multiple documents with optional filtering, sorting, or pagination. | | `findOne(documentID, queryParams?)` | Retrieve a single document by its unique ID. | | `create(data, queryParams?)` | Create a new document in the collection. | | `update(documentID, data, queryParams?)` | Update an existing document. | | `delete(documentID, queryParams?)` | Delete an existing document. | **Usage examples:** ```js const articles = client.collection('articles'); // Fetch all english articles sorted by title const allArticles = await articles.find({ locale: 'en', sort: 'title', }); // Fetch a single article const singleArticle = await articles.findOne('article-document-id'); // Create a new article const newArticle = await articles.create({ title: 'New Article', content: '...' }); // Update an existing article const updatedArticle = await articles.update('article-document-id', { title: 'Updated Title' }); // Delete an article await articles.delete('article-id'); ``` ### Working with single types Single types in Strapi represent unique content entries that exist only once (e.g., the homepage settings or site-wide configurations). The Strapi Client provides a `single()` method to interact with these resources, with the following methods available: | Parameter | Description | | ----------| -------------------------------------------------------------------------------------------- | | `find(queryParams?)` | Fetch the document. | | `update(documentID, data, queryParams?)` | Update the document. | | `delete(queryParams?)` | Remove the document. | **Usage examples:** ```js const homepage = client.single('homepage'); // Fetch the default homepage content const defaultHomepage = await homepage.find(); // Fetch the Spanish version of the homepage const spanishHomepage = await homepage.find({ locale: 'es' }); // Update the homepage draft content const updatedHomepage = await homepage.update( { title: 'Updated Homepage Title' }, { status: 'draft' } ); // Delete the homepage content await homepage.delete(); ``` ### Working with files The Strapi Client provides access to the [Media Library](/cms/features/media-library) via the `files` property. This allows you to retrieve and manage file metadata without directly interacting with the REST API. The following methods are available for working with files. Click on the method name in the table to jump to the corresponding section with more details and examples: | Method | Description | |--------|-------------| | [`find(params?)`](#find) | Retrieves a list of file metadata based on optional query parameters | | [`findOne(fileId)`](#findone) | Retrieves the metadata for a single file by its ID | | [`update(fileId, fileInfo)`](#update) | Updates metadata for an existing file | | [`upload(file, options)`](#upload) | Uploads a file (Blob or Buffer) with an optional `options` object for metadata | | [`delete(fileId)`](#delete) | Deletes a file by its ID | #### `find` The `strapi.client.files.find()` method retrieves a list of file metadata based on optional query parameters. The method can be used as follows: ```js // Initialize the client const client = strapi({ baseURL: 'http://localhost:1337/api', auth: 'your-api-token', }); // Find all file metadata const allFiles = await client.files.find(); console.log(allFiles); // Find file metadata with filtering and sorting const imageFiles = await client.files.find({ filters: { mime: { $contains: 'image' }, // Only get image files name: { $contains: 'avatar' }, // Only get files with 'avatar' in the name }, sort: ['name:asc'], // Sort by name in ascending order }); ``` #### `findOne` {#findone} The `strapi.client.files.findOne()` method retrieves the metadata for a single file by its id. The method can be used as follows: ```js // Initialize the client const client = strapi({ baseURL: 'http://localhost:1337/api', auth: 'your-api-token', }); // Find file metadata by ID const file = await client.files.findOne(1); console.log(file.name); console.log(file.url); console.log(file.mime); // The file MIME type ``` #### `update` The `strapi.client.files.update()` method updates metadata for an existing file, accepting 2 parameters, the `fileId`, and an object containing options such as the name, alternative text, and caption for the media. The methods can be used as follows: ```js // Initialize the client const client = strapi({ baseURL: 'http://localhost:1337/api', auth: 'your-api-token', }); // Update file metadata const updatedFile = await client.files.update(1, { name: 'New file name', alternativeText: 'Descriptive alt text for accessibility', caption: 'A caption for the file', }); ``` #### `upload` {#upload} The Strapi Client provides media file upload functionality through the `FilesManager`, accessible through the `strapi.client.files.upload()` method. The method allows you to upload media files (such as images, videos, or documents) to your Strapi backend. The method supports uploading files as `Blob` (in browsers or Node.js) or as `Buffer` (in Node.js only). The method also supports attaching metadata to the uploaded file, such as `alternativeText` and `caption`. ##### Method Signature ```js async upload(file: Blob, options?: BlobUploadOptions): Promise async upload(file: Buffer, options: BufferUploadOptions): Promise ``` - For `Blob` uploads, `options` is optional and may include `fileInfo` for metadata. - For `Buffer` uploads, `options` must include `filename` and `mimetype`, and may include `fileInfo`. The response is an array of file objects, each containing details such as `id`, `name`, `url`, `size`, and `mime` [source](https://github.com/strapi/client/blob/60a0117e361346073bed1959d354c7facfb963b3/src/files/types.ts). You can upload a file use through the browser as follows: ```js const client = strapi({ baseURL: 'http://localhost:1337/api' }); const fileInput = document.querySelector('input[type="file"]'); const file = fileInput.files[0]; try { const result = await client.files.upload(file, { fileInfo: { alternativeText: 'A user uploaded image', caption: 'Uploaded via browser', }, }); console.log('Upload successful:', result); } catch (error) { console.error('Upload failed:', error); } ``` With Node.js, you can either upload a blob or a buffer, as in the following examples: ```js const client = strapi({ baseURL: 'http://localhost:1337/api' }); const filePath = './image.png'; const mimeType = 'image/png'; const fileContentBuffer = await readFile(filePath); const fileBlob = new Blob([fileContentBuffer], { type: mimeType }); try { const result = await client.files.upload(fileBlob, { fileInfo: { name: 'Image uploaded as Blob', alternativeText: 'Uploaded from Node.js Blob', caption: 'Example upload', }, }); console.log('Blob upload successful:', result); } catch (error) { console.error('Blob upload failed:', error); } ``` ```js const client = strapi({ baseURL: 'http://localhost:1337/api' }); const filePath = './image.png'; const fileContentBuffer = await readFile(filePath); try { const result = await client.files.upload(fileContentBuffer, { filename: 'image.png', mimetype: 'image/png', fileInfo: { name: 'Image uploaded as Buffer', alternativeText: 'Uploaded from Node.js Buffer', caption: 'Example upload', }, }); console.log('Buffer upload successful:', result); } catch (error) { console.error('Buffer upload failed:', error); } ``` ##### Response Structure The `strapi.client.files.upload()` method returns an array of file objects, each with fields such as: ```json { "id": 1, "name": "image.png", "alternativeText": "Uploaded from Node.js Buffer", "caption": "Example upload", "mime": "image/png", "url": "/uploads/image.png", "size": 12345, "createdAt": "2025-07-23T12:34:56.789Z", "updatedAt": "2025-07-23T12:34:56.789Z" } ``` :::note Additional response fields The upload response includes additional fields beyond those shown above. See the complete FileResponse interface in the [client source code](https://github.com/strapi/client/blob/main/src/files/types.ts) for all available fields. ::: #### `delete` The `strapi.client.files.delete()` method deletes a file by its ID. The method can be used as follows: ```js // Initialize the client const client = strapi({ baseURL: 'http://localhost:1337/api', auth: 'your-api-token', }); // Delete a file by ID const deletedFile = await client.files.delete(1); console.log('File deleted successfully'); console.log('Deleted file ID:', deletedFile.id); console.log('Deleted file name:', deletedFile.name); ```
## Handling Common Errors The following errors might occur when sending queries through the Strapi Client: | Error | Description | |-------|-------------| | Permission Errors | If the authenticated user does not have permission to upload or manage files, a `FileForbiddenError` is thrown. | | HTTP Errors|If the server is unreachable, authentication fails, or there are network issues, an `HTTPError` is thrown. | | Missing Parameters|When uploading a `Buffer`, both `filename` and `mimetype` must be provided in the options object. If either is missing, an error is thrown. | :::strapi Additional information More details about the Strapi Client may be found in the [package](https://github.com/strapi/client/blob/main/README.md). ::: # Content API Source: https://docs.strapi.io/cms/api/content-api # Strapi APIs to access your content Strapi's Content API provides access to your content through REST and GraphQL APIs for front-end applications, plus lower-level Document Service and Query Engine APIs for backend and plugin development. Once you've created and configured a Strapi project, created a content structure with the [Content-Type Builder](/cms/features/content-type-builder) and started adding data through the [Content Manager](/cms/features/content-manager), you likely would like to access your content. From a front-end application, your content can be accessed through Strapi's Content API, which is exposed: - by default through the [REST API](/cms/api/rest) - and also through the [GraphQL API](/cms/api/graphql) if you installed the Strapi built-in [GraphQL plugin](/cms/plugins/graphql). You can also use the [Strapi Client](/cms/api/client) library to interact with the REST API. REST and GraphQL APIs represent the top-level layers of the Content API exposed to external applications. Strapi also provides 2 lower-level APIs: - The [Document Service API](/cms/api/document-service), accessible through `strapi.documents`, is the recommended API to interact with your application's database within the [backend server](/cms/customization) or through [plugins](/cms/plugins-development/developing-plugins). The Document Service is the layer that handles **documents** as well as Strapi's complex content structures like components and dynamic zones. - The [Query Engine API](/cms/api/query-engine), accessible through `db.query` (i.e., `strapi.db.query`), interacts with the database layer at a lower level and is used under the hood to execute database queries. It gives unrestricted internal access to the database layer, but is not aware of any advanced Strapi features that Strapi 5 can handle, like Draft & Publish, Internationalization, Content History, and more.
⚠️ In most, if not all, use cases, you should use the Document Service API instead.
This documentation section includes reference information about the following Strapi APIs and some integration guides with 3rd party technologies: - [REST API](/cms/api/rest): Query the Content API from a front-end application through REST. - [GraphQL API](/cms/api/graphql): Query the Content API from a front-end application through GraphQL. - [Strapi Client](/cms/api/client): Interact with the REST API through the Strapi Client library. - [Document Service API](/cms/api/document-service): Query your data through the backend server or plugins. - [OpenAPI Specification](/cms/api/openapi): Generate OpenAPI specifications for your Strapi applications. :::strapi Integrations If you're looking for how to integrate Strapi with other platforms, such as [Next.js](https://strapi.io/integrations/nextjs-cms), [Astro](https://strapi.io/integrations/astro), [Angular](https://strapi.io/integrations/angular-cms), and more, please refer to Strapi's [integrations pages](https://strapi.io/integrations). ::: # Documents Source: https://docs.strapi.io/cms/api/document
# Documents A document is an API-only concept representing all content variations (locales, draft/published versions) for a single content-type entry. Use the Document Service API to manipulate documents on the back-end. A **document** in Strapi 5 is an API-only concept. A document represents all the different variations of content for a given entry of a content-type. A single type contains a unique document, and a collection type can contain several documents. When you use the admin panel, the concept of a document is never mentioned and not necessary for the end user. Users create and edit **entries** in the [Content Manager](/cms/features/content-manager). For instance, as a user, you either list the entries for a given locale, or edit the draft version of a specific entry in a given locale. However, at the API level, the value of the fields of an entry can actually have: - different content for the English and the French locale, - and even different content for the draft and published version in each locale. The bucket that includes the content of all the draft and published versions for all the locales is a document. Manipulating documents with the [Document Service API](/cms/api/document-service) will help you create, retrieve, update, and delete documents or a specific subset of the data they contain. The following diagrams represent all the possible variations of content depending on which features, such as [Internationalization (i18n)](/cms/features/internationalization) and [Draft & Publish](/cms/features/draft-and-publish), are enabled for a content-type: - If the Internationalization (i18n) feature is enabled on the content-type, a document can have multiple **document locales**. - If the Draft & Publish feature is enabled on the content-type, a document can have a **published** and a **draft** version. :::strapi APIs to query documents data To interact with documents or the data they represent: - From the back-end server (for instance, from controllers, services, and the back-end part of plugins), use the [Document Service API](/cms/api/document-service). - From the front-end part of your application, query your data using the [REST API](/cms/api/rest) or the [GraphQL API](/cms/api/graphql). For additional information about the APIs, please refer to the [Content API introduction](/cms/api/content-api). ::: :::info Default version in returned results An important difference between the back-end and front-end APIs is about the default version returned when no parameter is passed: - The Document Service API returns the draft version by default, - while REST and GraphQL APIs return the published version by default. :::
# Document Service API Source: https://docs.strapi.io/cms/api/document-service # Document Service API The Document Service API is the recommended backend API for interacting with content, providing `findOne`, `findMany`, `create`, `update`, and `delete` methods that work with stable `documentId` identifiers and support Draft & Publish operations. The Document Service API is built on top of the **Query Engine API** 2 different back-end APIs allow you to interact with your content:
  • The [Query Engine API](/cms/api/query-engine) is the lower-level layer that offers unrestricted access to the database, but is not aware of complex Strapi content structures such as components and dynamic zones.
  • The Document Service API is built on top of the Query Engine and is the recommended way to interact with your content while you are customizing the back end server or developing plugins.
More details can be found in the [Content API](/cms/api/content-api) and [backend customization](/cms/backend-customization) introductions. and is used to perform CRUD ([create](#create), [retrieve](#findone), [update](#update), and [delete](#delete)) operations on **documents** . The Document Service API also supports [counting](#count) documents and, if [Draft & Publish](/cms/features/draft-and-publish) is enabled on the content-type, performing Strapi-specific operations such as [publishing](#publish), [unpublishing](#unpublish), and [discarding drafts](#discarddraft). In Strapi 5, documents are uniquely identified by their `documentId` at the API level. **`documentId` explained: Replacing `id` from Strapi v4** In previous Strapi versions, the concept of `id` (used both in the Content API and as the database row identifier) was not always stable: a single entry could have multiple versions or localizations, and its numeric identifier `id` could change in cases such as duplication or import/export operations. To address this limitation, Strapi 5 introduced `documentId`, a 24-character alphanumeric string, as a unique and persistent identifier for a content entry, independent of its physical records. This new identifier is used internally in Strapi 5 to manage relationships, publishing, localization, and version history, as all possible variations of a content entry are now grouped under a single [document](/cms/api/document) concept. As a result, starting with Strapi 5, many APIs and services rely on `documentId` instead of `id` to ensure consistency across operations. Some APIs may still return both `documentId` and `id` to ease the transition, but using `documentId` for content queries is strongly recommended, as `documentId` might be the only identifier used in future Strapi versions. For more details on the transition from `id` to `documentId`, refer to the [breaking change page](/cms/migration/v4-to-v5/breaking-changes/use-document-id) and the [migration guide from Entity Service to Document Service API](/cms/migration/v4-to-v5/additional-resources/from-entity-service-to-document-service). :::strapi Entity Service API is deprecated in Strapi 5 The Document Service API replaces the Entity Service API used in Strapi v4 ([see Strapi v4 documentation](https://docs-v4.strapi.io/dev-docs/api/entity-service)). Additional information on how to migrate from the Entity Service API to the Document Service API can be found in the [migration reference](/cms/migration/v4-to-v5/additional-resources/from-entity-service-to-document-service). ::: :::note Relations can also be connected, disconnected, and set through the Document Service API, just like with the REST API (see the [REST API relations documentation](/cms/api/rest/relations) for examples). ::: :::caution Document Service returns unsanitized data The Document Service is a data-access layer: it interacts with the database and is not aware of user permissions or field visibility. Results may include private fields, passwords, and restricted relations. The built-in REST and GraphQL APIs automatically sanitize responses before sending them to the client. But if you build custom controllers or plugin routes that call Document Service methods directly, you must sanitize the output yourself before returning it. Use `strapi.contentAPI.sanitize.output()` in your controller (see [Sanitization and validation when building custom controllers](/cms/backend-customization/controllers#sanitize-validate-custom-controllers) for details and code examples). ::: ## Configuration The `documents.strictParams` option enables strict validation of parameters passed to Document Service methods such as `findMany` and `findOne`. Configure it in the [API configuration](/cms/configurations/api) file (`./config/api.js` or `./config/api.ts`). See the [API configuration](/cms/configurations/api) table for details on `documents.strictParams`. ## Document objects Document methods return a document object or a list of document objects, which represent a version of a content entry grouped under a stable `documentId`. Returned objects typically include: - `documentId`: Persistent identifier for the entry across locales and draft/published versions. - `id`: Database identifier for the specific locale/version record. - model fields: All fields defined in the content-type schema. Relations, components, and dynamic zones are not populated unless you opt in with `populate` (see [Populating fields](/cms/api/document-service/populate)) or limit fields with `fields` (see [Selecting fields](/cms/api/document-service/fields)). - metadata: `publishedAt`, `createdAt`, `updatedAt`, and `createdBy`/`updatedBy` when available. Optionally, document objects can also include a `status` and `locale` property if [Draft & Publish](/cms/features/draft-and-publish) and [Internationalization](/cms/features/internationalization) are enabled for the content-type. ## Method overview Each section below documents the parameters and examples for a specific method: | Method | Purpose | | --- | --- | | [`findOne()`](#findone) | Fetch a document by `documentId`, optionally scoping to a locale or status. | | [`findFirst()`](#findfirst) | Return the first document that matches filters. | | [`findMany()`](#findmany) | List documents with filters, sorting, and pagination. | | [`create()`](#create) | Create a document, optionally targeting a locale. | | [`update()`](#update) | Update a document by `documentId`. | | [`delete()`](#delete) | Delete a document or a specific locale version. | | [`deleteMany()`](#deletemany) | Delete multiple documents matching filters and relation parameters. | | [`publish()`](#publish) | Publish the draft version of a document. | | [`unpublish()`](#unpublish) | Move a published document back to draft. | | [`discardDraft()`](#discarddraft) | Drop draft data and keep only the published version. | | [`count()`](#count) | Count how many documents match the parameters. | :::note Draft & Publish method availability The [`publish()`](#publish), [`unpublish()`](#unpublish), and [`discardDraft()`](#discarddraft) methods are only available when the Draft & Publish feature is enabled on the content-type. Calling these methods on a content-type that does not have Draft & Publish enabled will throw an error. To enable Draft & Publish, see the [Draft & Publish documentation](/cms/features/draft-and-publish). ::: ### `findOne()` Syntax: `findOne(parameters: Params) => Document` #### GET strapi.documents().findOne() — findOne() Find a document matching the passed documentId and parameters. If only a documentId is passed without any other parameters, findOne() returns the draft version of a document in the default locale. Returns the matching document if found, otherwise returns null. **Parameters:** - `documentId` (ID, required): Document id - `locale` (String or undefined): Locale of the document to find. Defaults to the default locale. See locale docs (/cms/api/document-service/locale#find-one). - `status` (): If Draft & Publish (/cms/features/draft-and-publish) is enabled: publication status. Can be `published` or `draft`. Default: `draft`. See status docs (/cms/api/document-service/status#find-one). - `publicationFilter` (String): If Draft & Publish (/cms/features/draft-and-publish) is enabled: derived publication cohort to match before applying `status`. See publicationFilter docs (/cms/api/document-service/publication-filter). - `fields` (Object): Select fields (/cms/api/document-service/fields#findone) to return. Defaults to all fields (except those not populated by default). - `populate` (Object): Populate (/cms/api/document-service/populate) results with additional fields. Default: `null`. **Request:** ``` await strapi.documents('api::restaurant.restaurant').findOne({ documentId: 'a1b2c3d4e5f6g7h8i9j0klmn' }) ``` **Response 200 OK:** ```json { "documentId": "a1b2c3d4e5f6g7h8i9j0klmn", "name": "Biscotte Restaurant", "publishedAt": null, "locale": "en" } ``` ### `findFirst()` Syntax: `findFirst(parameters: Params) => Document` #### GET strapi.documents().findFirst() — findFirst() Find the first document matching the parameters. By default, findFirst() returns the draft version, in the default locale, of the first document for the passed unique identifier (collection type id or single type id). **Parameters:** - `locale` (String or undefined): Locale of the documents to find. Defaults to the default locale. See locale docs (/cms/api/document-service/locale#find-first). - `status` (): If Draft & Publish (/cms/features/draft-and-publish) is enabled: publication status. Can be `published` or `draft`. Default: `draft`. See status docs (/cms/api/document-service/status#find-first). - `publicationFilter` (String): If Draft & Publish (/cms/features/draft-and-publish) is enabled: derived publication cohort to match before applying `status`. See publicationFilter docs (/cms/api/document-service/publication-filter). - `filters` (Object): Filters (/cms/api/document-service/filters) to use. Default: `null`. - `fields` (Object): Select fields (/cms/api/document-service/fields#findfirst) to return. Defaults to all fields (except those not populated by default). - `populate` (Object): Populate (/cms/api/document-service/populate) results with additional fields. Default: `null`. **Generic example:** ``` await strapi.documents('api::restaurant.restaurant').findFirst() ``` **With filters:** ``` await strapi.documents('api::restaurant.restaurant').findFirst( { filters: { name: { $startsWith: "Pizzeria" } } } ) ``` **Response 200 Generic:** ```json { "documentId": "a1b2c3d4e5f6g7h8i9j0klmn", "name": "Restaurant Biscotte", "publishedAt": null, "locale": "en" } ``` **Response 200 With filters:** ```json { "documentId": "j9k8l7m6n5o4p3q2r1s0tuvw", "name": "Pizzeria Arrivederci", "publishedAt": null, "locale": "en" } ``` If no `locale` or `status` parameters are passed, results return the draft version for the default locale. ### `findMany()` Syntax: `findMany(parameters: Params) => Document[]` #### GET strapi.documents().findMany() — findMany() Find documents matching the parameters. When no parameter is passed, findMany() returns the draft version in the default locale for each document. **Parameters:** - `locale` (String or undefined): Locale of the documents to find. Defaults to the default locale. See locale docs (/cms/api/document-service/locale#find-many). - `status` (): If Draft & Publish (/cms/features/draft-and-publish) is enabled: publication status. Can be `published` or `draft`. Default: `draft`. See status docs (/cms/api/document-service/status#find-many). - `publicationFilter` (String): If Draft & Publish (/cms/features/draft-and-publish) is enabled: derived publication cohort to match before applying `status`. See publicationFilter docs (/cms/api/document-service/publication-filter). - `filters` (Object): Filters (/cms/api/document-service/filters) to use. Default: `null`. - `fields` (Object): Select fields (/cms/api/document-service/fields#findmany) to return. Defaults to all fields (except those not populated by default). - `populate` (Object): Populate (/cms/api/document-service/populate) results with additional fields. Default: `null`. - `pagination` (Object): Paginate (/cms/api/document-service/sort-pagination#pagination) results. - `sort` (Object): Sort (/cms/api/document-service/sort-pagination#sort) results. **Generic example:** ``` await strapi.documents('api::restaurant.restaurant').findMany() ``` **With filters:** ``` await strapi.documents('api::restaurant.restaurant').findMany( { filters: { name: { $startsWith: 'Pizzeria' } } } ) ``` **Response 200 Generic:** ```json [ { "documentId": "a1b2c3d4e5f6g7h8i9j0klmn", "name": "Biscotte Restaurant", "publishedAt": null, "locale": "en" }, { "documentId": "j9k8l7m6n5o4p3q2r1s0tuvw", "name": "Pizzeria Arrivederci", "publishedAt": null, "locale": "en" } ] ``` **Response 200 With filters:** ```json [ { "documentId": "j9k8l7m6n5o4p3q2r1s0tuvw", "name": "Pizzeria Arrivederci", "locale": "en", "publishedAt": null } ] ``` Available filters are detailed in the [filters](/cms/api/document-service/filters) page of the Document Service API reference. If no `locale` or `status` parameters are passed, results return the draft version for the default locale. ### `create()` Syntax: `create(parameters: Params) => Document` #### GET strapi.documents().create() — create() Create a new document. If no locale parameter is passed, create() creates the draft version of the document for the default locale. **Parameters:** - `locale` (String or undefined): Locale of the document to create. Defaults to the default locale. See locale docs (/cms/api/document-service/locale#create). - `fields` (Object): Select fields (/cms/api/document-service/fields#create) to return. Defaults to all fields (except those not populated by default). - `status` (): If Draft & Publish (/cms/features/draft-and-publish) is enabled: can be set to `published` to automatically publish the draft version of a document while creating it. See status docs (/cms/api/document-service/status#create). - `populate` (Object): Populate (/cms/api/document-service/populate) results with additional fields. Default: `null`. **Request:** ``` await strapi.documents('api::restaurant.restaurant').create({ data: { name: 'Restaurant B' } }) ``` **Response 200 OK:** ```json { "documentId": "a1b2c3d4e5f6g7h8i9j0klmn", "name": "Restaurant B", "publishedAt": null, "locale": "en" } ``` :::tip If the [Draft & Publish](/cms/features/draft-and-publish) feature is enabled on the content-type, you can automatically publish a document while creating it (see [`status` documentation](/cms/api/document-service/status#create)). ::: ### `update()` Syntax: `update(parameters: Params) => Promise` #### GET strapi.documents().update() — update() Update a document by documentId. If no locale parameter is passed, update() updates the document for the default locale. **Parameters:** - `documentId` (ID, required): Document id - `locale` (String or null): Locale of the document to update. Defaults to the default locale. See locale docs (/cms/api/document-service/locale#update). - `filters` (Object): Filters (/cms/api/document-service/filters) to use. Default: `null`. - `fields` (Object): Select fields (/cms/api/document-service/fields#update) to return. Defaults to all fields (except those not populated by default). - `status` (): If Draft & Publish (/cms/features/draft-and-publish) is enabled: can be set to `published` to automatically publish the draft version of a document while updating it. See status docs (/cms/api/document-service/status#update). - `populate` (Object): Populate (/cms/api/document-service/populate) results with additional fields. Default: `null`. **Request:** ``` await strapi.documents('api::restaurant.restaurant').update({ documentId: 'a1b2c3d4e5f6g7h8i9j0klmn', data: { name: "New restaurant name" } }) ``` **Response 200 OK:** ```json { "documentId": "a1b2c3d4e5f6g7h8i9j0klmn", "name": "New restaurant name", "locale": "en", "publishedAt": null } ``` :::tip Published versions are read-only, so you can not technically update the published version of a document. To update a document and publish the new version right away, you can: - update its draft version with `update()`, then [publish it](#publish) with `publish()`, - or directly add `status: 'published'` along with the other parameters passed to `update()` (see [`status` documentation](/cms/api/document-service/status#update)). ::: :::caution It's not recommended to update repeatable components with the Document Service API (see the related [breaking change entry](/cms/migration/v4-to-v5/breaking-changes/do-not-update-repeatable-components-with-document-service-api.md) for more details). ::: ### `delete()` Syntax: `delete(parameters: Params): Promise<{ documentId: ID, entries: Number }>` #### GET strapi.documents().delete() — delete() Delete a document or a specific locale version. If no locale parameter is passed, delete() only deletes the default locale version of a document. This deletes both the draft and published versions. **Parameters:** - `documentId` (ID, required): Document id - `locale` (String, ): Locale version of the document to delete. Default: `null` (deletes only the default locale). See locale docs (/cms/api/document-service/locale#delete). - `filters` (Object): Filters (/cms/api/document-service/filters) to use. Default: `null`. - `fields` (Object): Select fields (/cms/api/document-service/fields#delete) to return. Defaults to all fields (except those not populated by default). - `populate` (Object): Populate (/cms/api/document-service/populate) results with additional fields. Default: `null`. **Request:** ``` await strapi.documents('api::restaurant.restaurant').delete({ documentId: 'a1b2c3d4e5f6g7h8i9j0klmn', }) ``` **Response 200 OK:** ```json { "documentId": "a1b2c3d4e5f6g7h8i9j0klmn", "entries": [ { "documentId": "a1b2c3d4e5f6g7h8i9j0klmn", "name": "Biscotte Restaurant", "publishedAt": "2024-03-14T18:30:48.870Z", "locale": "en" } ] } ``` ### `deleteMany()` Syntax: `deleteMany(parameters: Params): Promise<{ documentId: ID, entries: Number }>` #### GET strapi.documents().deleteMany() — deleteMany() Delete multiple documents matching filters and relation parameters. **Parameters:** - `locale` (String, ): Locale version of documents to delete. Default: only the default locale. See locale docs (/cms/api/document-service/locale#delete). - `filters` (Object): Filters (/cms/api/document-service/filters) to use. Default: `null`. - `fields` (Object): Select fields (/cms/api/document-service/fields#delete) to return. Defaults to all fields (except those not populated by default). - `populate` (Object): Populate (/cms/api/document-service/populate) results with additional fields. Default: `null`. **Request:** ``` await strapi.documents('api::restaurant.restaurant').deleteMany({ filters: { city: { name: { $eq: 'New York' } } } }); ``` **Response 200 OK:** ```json { "documentId": "multiple_documents", "entries": 3 } ``` ### `publish()` Syntax: `publish(parameters: Params): Promise<{ documentId: ID, entries: Number }>` #### GET strapi.documents().publish() — publish() Publish the draft version of a document. This method is only available if Draft & Publish is enabled on the content-type. If no locale parameter is passed, publish() only publishes the default locale version of the document. **Parameters:** - `documentId` (ID, required): Document id - `locale` (String, ): Locale of the documents to publish. Default: only the default locale. See locale docs (/cms/api/document-service/locale#publish). - `filters` (Object): Filters (/cms/api/document-service/filters) to use. Default: `null`. - `fields` (Object): Select fields (/cms/api/document-service/fields#publish) to return. Defaults to all fields (except those not populated by default). - `populate` (Object): Populate (/cms/api/document-service/populate) results with additional fields. Default: `null`. **Request:** ``` await strapi.documents('api::restaurant.restaurant').publish({ documentId: 'a1b2c3d4e5f6g7h8i9j0klmn', }); ``` **Response 200 OK:** ```json { "documentId": "a1b2c3d4e5f6g7h8i9j0klmn", "entries": [ { "documentId": "a1b2c3d4e5f6g7h8i9j0klmn", "name": "Biscotte Restaurant", "publishedAt": "2024-03-14T18:30:48.870Z", "locale": "en" } ] } ``` ### `unpublish()` Syntax: `unpublish(parameters: Params): Promise<{ documentId: ID, entries: Number }>` #### GET strapi.documents().unpublish() — unpublish() Move a published document back to draft. This method is only available if Draft & Publish is enabled on the content-type. If no locale parameter is passed, unpublish() only unpublishes the default locale version of the document. **Parameters:** - `documentId` (ID, required): Document id - `locale` (String, ): Locale of the documents to unpublish. Default: only the default locale. See locale docs (/cms/api/document-service/locale#unpublish). - `filters` (Object): Filters (/cms/api/document-service/filters) to use. Default: `null`. - `fields` (Object): Select fields (/cms/api/document-service/fields#unpublish) to return. Defaults to all fields (except those not populated by default). - `populate` (Object): Populate (/cms/api/document-service/populate) results with additional fields. Default: `null`. **Request:** ``` await strapi.documents('api::restaurant.restaurant').unpublish({ documentId: 'a1b2c3d4e5f6g7h8i9j0klmn' }); ``` **Response 200 OK:** ```json { "documentId": "a1b2c3d4e5f6g7h8i9j0klmn", "entries": [ { "documentId": "a1b2c3d4e5f6g7h8i9j0klmn", "name": "Biscotte Restaurant", "publishedAt": null, "locale": "en" } ] } ``` ### `discardDraft()` Syntax: `discardDraft(parameters: Params): Promise<{ documentId: ID, entries: Number }>` #### GET strapi.documents().discardDraft() — discardDraft() Drop draft data and keep only the published version. This method is only available if Draft & Publish is enabled on the content-type. If no locale parameter is passed, discardDraft() discards draft data and overrides it with the published version only for the default locale. **Parameters:** - `documentId` (ID, required): Document id - `locale` (String, ): Locale of the documents to discard. Default: only the default locale. See locale docs (/cms/api/document-service/locale#discard-draft). - `filters` (Object): Filters (/cms/api/document-service/filters) to use. Default: `null`. - `fields` (Object): Select fields (/cms/api/document-service/fields#discarddraft) to return. Defaults to all fields (except those not populated by default). - `populate` (Object): Populate (/cms/api/document-service/populate) results with additional fields. Default: `null`. **Request:** ``` strapi.documents('api::restaurant.restaurant').discardDraft({ documentId: 'a1b2c3d4e5f6g7h8i9j0klmn', }); ``` **Response 200 OK:** ```json { "documentId": "a1b2c3d4e5f6g7h8i9j0klmn", "entries": [ { "documentId": "a1b2c3d4e5f6g7h8i9j0klmn", "name": "Biscotte Restaurant", "publishedAt": null, "locale": "en" } ] } ``` ### `count()` Syntax: `count(parameters: Params) => number` #### GET strapi.documents().count() — count() Count how many documents match the parameters. If no parameter is passed, the count() method returns the total number of documents for the default locale. **Parameters:** - `locale` (String or null): Locale of the documents to count. Defaults to the default locale. See locale docs (/cms/api/document-service/locale#count). - `status` (): If Draft & Publish (/cms/features/draft-and-publish) is enabled: publication status. `published` to count only published documents, `draft` to count draft documents (returns all documents). Default: `draft`. See status docs (/cms/api/document-service/status#count). - `publicationFilter` (String): If Draft & Publish (/cms/features/draft-and-publish) is enabled: derived publication cohort to match before applying `status`. See publicationFilter docs (/cms/api/document-service/publication-filter). - `filters` (Object): Filters (/cms/api/document-service/filters) to use. Default: `null`. **Generic example:** ``` await strapi.documents('api::restaurant.restaurant').count() ``` **Count published:** ``` strapi.documents('api::restaurant.restaurant').count({ status: 'published' }) ``` **With filters:** ``` /** * Count number of draft documents (default if status is omitted) * in English (default locale) * whose name starts with 'Pizzeria' */ strapi.documents('api::restaurant.restaurant').count({ filters: { name: { $startsWith: "Pizzeria" }}}) ``` :::note Since published documents necessarily also have a draft counterpart, a published document is still counted as having a draft version. This means that counting with the `status: 'draft'` parameter still returns the total number of documents matching other parameters, even if some documents have already been published and are not displayed as "draft" or "modified" in the Content Manager anymore. To count only never-published drafts, pass a [`publicationFilter`](/cms/api/document-service/publication-filter) value such as `'never-published'` or `'never-published-document'`. ::: # Using fields with the Document Service API Source: https://docs.strapi.io/cms/api/document-service/fields # Document Service API: Selecting fields Use the `fields` parameter in Document Service API queries to select specific fields to return with your results, reducing data payload across `findOne()`, `findMany()`, `create()`, `update()`, `delete()`, `publish()`, and other document operations. By default the [Document Service API](/cms/api/document-service) returns all the fields of a document but does not populate any fields. This page describes how to use the `fields` parameter to return only specific fields with the query results. :::tip You can also use the `populate` parameter to populate relations, media fields, components, or dynamic zones (see the [`populate` parameter](/cms/api/document-service/populate) documentation). ::: :::note Though it's recommended to target entries by their `documentId` in Strapi 5, entries might still have an `id` field, and you will see it in the returned response. This should ease your transition from Strapi 4. Please refer to the [breaking change entry](/cms/migration/v4-to-v5/breaking-changes/use-document-id) for more details. ::: ## Select fields with `findOne()` queries {#findone} #### GET strapi.documents( — Select fields with findOne() Select specific fields to return when finding a document by documentId. **JavaScript:** ``` const document = await strapi.documents("api::restaurant.restaurant").findOne({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', fields: ["name", "description"], }); ``` **Response 200 OK:** ```json { documentId: "a1b2c3d4e5f6g7h8i9j0klm", name: "Biscotte Restaurant", description: "Welcome to Biscotte restaurant! …" } ``` ## Select fields with `findFirst()` queries {#findfirst} #### GET strapi.documents( — Select fields with findFirst() Select specific fields to return when finding the first matching document. **JavaScript:** ``` const document = await strapi.documents("api::restaurant.restaurant").findFirst({ fields: ["name", "description"], }); ``` **Response 200 OK:** ```json { documentId: "a1b2c3d4e5f6g7h8i9j0klm", name: "Biscotte Restaurant", description: "Welcome to Biscotte restaurant! …" } ``` ## Select fields with `findMany()` queries {#findmany} #### GET strapi.documents( — Select fields with findMany() Select specific fields to return when finding multiple documents. **JavaScript:** ``` const documents = await strapi.documents("api::restaurant.restaurant").findMany({ fields: ["name", "description"], }); ``` **Response 200 OK:** ```json [ { documentId: "a1b2c3d4e5f6g7h8i9j0klm", name: "Biscotte Restaurant", description: "Welcome to Biscotte restaurant! …" } // ... ] ``` ## Select fields with `create()` queries {#create} #### GET strapi.documents( — Select fields with create() Select specific fields to return when creating a new document. **JavaScript:** ``` const document = await strapi.documents("api::restaurant.restaurant").create({ data: { name: "Restaurant B", description: "Description for the restaurant", }, fields: ["name", "description"], }); ``` **Response 200 OK:** ```json { id: 4, documentId: 'fmtr6d7ktzpgrijqaqgr6vxs', name: 'Restaurant B', description: 'Description for the restaurant' } ``` ## Select fields with `update()` queries {#update} #### GET strapi.documents( — Select fields with update() Select specific fields to return when updating a document. **JavaScript:** ``` const document = await strapi.documents("api::restaurant.restaurant").update({ documentId: "fmtr6d7ktzpgrijqaqgr6vxs", data: { name: "Restaurant C", }, fields: ["name"], }); ``` **Response 200 OK:** ```json { documentId: 'fmtr6d7ktzpgrijqaqgr6vxs', name: 'Restaurant C' } ``` ## Select fields with `delete()` queries {#delete} #### GET strapi.documents( — Select fields with delete() Select specific fields to return when deleting a document. **JavaScript:** ``` const document = await strapi.documents("api::restaurant.restaurant").delete({ documentId: "fmtr6d7ktzpgrijqaqgr6vxs", fields: ["name"], }); ``` **Response 200 OK:** ```json documentId: 'fmtr6d7ktzpgrijqaqgr6vxs', // All of the deleted document's versions are returned entries: [ { id: 4, documentId: 'fmtr6d7ktzpgrijqaqgr6vxs', name: 'Restaurant C', // … } ] } ``` ## Select fields with `publish()` queries {#publish} #### GET strapi.documents( — Select fields with publish() Select specific fields to return when publishing a document. **JavaScript:** ``` const document = await strapi.documents("api::restaurant.restaurant").publish({ documentId: "fmtr6d7ktzpgrijqaqgr6vxs", fields: ["name"], }); ``` **Response 200 OK:** ```json { documentId: 'fmtr6d7ktzpgrijqaqgr6vxs', // All of the published locale entries are returned entries: [ { documentId: 'fmtr6d7ktzpgrijqaqgr6vxs', name: 'Restaurant B' } ] } ``` ## Select fields with `unpublish()` queries {#unpublish} #### GET strapi.documents( — Select fields with unpublish() Select specific fields to return when unpublishing a document. **JavaScript:** ``` const document = await strapi.documents("api::restaurant.restaurant").unpublish({ documentId: "cjld2cjxh0000qzrmn831i7rn", fields: ["name"], }); ``` **Response 200 OK:** ```json { documentId: 'fmtr6d7ktzpgrijqaqgr6vxs', // All of the published locale entries are returned entries: [ { documentId: 'fmtr6d7ktzpgrijqaqgr6vxs', name: 'Restaurant B' } ] } ``` ## Select fields with `discardDraft()` queries {#discarddraft} #### GET strapi.documents( — Select fields with discardDraft() Select specific fields to return when discarding a draft document. **JavaScript:** ``` const document = await strapi.documents("api::restaurant.restaurant").discardDraft({ documentId: "fmtr6d7ktzpgrijqaqgr6vxs", fields: ["name"], }); ``` **Response 200 OK:** ```json { documentId: "fmtr6d7ktzpgrijqaqgr6vxs", // All of the discarded draft entries are returned entries: [ { "name": "Restaurant B" } ] } ``` # Using filters with the Document Service API Source: https://docs.strapi.io/cms/api/document-service/filters # Document Service API: Filters The Document Service API provides attribute operators (`$eq`, `$lt`, `$contains`, etc.) and logical operators (`$and`, `$or`, `$not`) to filter query results with support for case-sensitive and case-insensitive matching. The [Document Service API](/cms/api/document-service) offers the ability to filter results. The following operators are available: | Operator | Description | | -------------------------------- | ---------------------------------------- | | [`$eq`](#eq) | Equal | | [`$eqi`](#eqi) | Equal (case-insensitive) | | [`$ne`](#ne) | Not equal | | [`$nei`](#nei) | Not equal (case-insensitive) | | [`$lt`](#lt) | Less than | | [`$lte`](#lte) | Less than or equal to | | [`$gt`](#gt) | Greater than | | [`$gte`](#gte) | Greater than or equal to | | [`$in`](#in) | Included in an array | | [`$notIn`](#notin) | Not included in an array | | [`$contains`](#contains) | Contains | | [`$notContains`](#notcontains) | Does not contain | | [`$containsi`](#containsi) | Contains (case-insensitive) | | [`$notContainsi`](#notcontainsi) | Does not contain (case-insensitive) | | [`$null`](#null) | Is null | | [`$notNull`](#notnull) | Is not null | | [`$between`](#between) | Is between | | [`$startsWith`](#startswith) | Starts with | | [`$startsWithi`](#startswithi) | Starts with (case-insensitive) | | [`$endsWith`](#endswith) | Ends with | | [`$endsWithi`](#endswithi) | Ends with (case-insensitive) | | [`$or`](#or) | Joins the filters in an "or" expression | | [`$and`](#and) | Joins the filters in an "and" expression | | [`$not`](#not) | Joins the filters in an "not" expression | :::strapi Deep filtering with the various APIs For examples of how to deep filter with the various APIs, please refer to [this blog article](https://strapi.io/blog/deep-filtering-alpha-26). ::: ## Attribute operators
#### GET strapi.documents().findMany() — $not Negates the nested condition(s). **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $not: { $contains: 'Hello World', }, }, }, }); ``` #### GET strapi.documents().findMany() — $eq Attribute equals input value. **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $eq: 'Hello World', }, }, }); ``` **Shorthand:** ``` // $eq can be omitted: const entries = await strapi.documents('api::article.article').findMany({ filters: { title: 'Hello World', }, }); ``` #### GET strapi.documents().findMany() — $eqi Attribute equals input value (case-insensitive). **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $eqi: 'HELLO World', }, }, }); ``` #### GET strapi.documents().findMany() — $ne Attribute does not equal input value. **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $ne: 'ABCD', }, }, }); ``` #### GET strapi.documents().findMany() — $nei Attribute does not equal input value (case-insensitive). **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $nei: 'abcd', }, }, }); ``` #### GET strapi.documents().findMany() — $in Attribute is contained in the input list. **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $in: ['Hello', 'Hola', 'Bonjour'], }, }, }); ``` **Shorthand:** ``` // $in can be omitted when passing an array of values: const entries = await strapi.documents('api::article.article').findMany({ filters: { title: ['Hello', 'Hola', 'Bonjour'], }, }); ``` #### GET strapi.documents().findMany() — $notIn Attribute is not contained in the input list. **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $notIn: ['Hello', 'Hola', 'Bonjour'], }, }, }); ``` #### GET strapi.documents().findMany() — $lt Attribute is less than the input value. **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { rating: { $lt: 10, }, }, }); ``` #### GET strapi.documents().findMany() — $lte Attribute is less than or equal to the input value. **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { rating: { $lte: 10, }, }, }); ``` #### GET strapi.documents().findMany() — $gt Attribute is greater than the input value. **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { rating: { $gt: 5, }, }, }); ``` #### GET strapi.documents().findMany() — $gte Attribute is greater than or equal to the input value. **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { rating: { $gte: 5, }, }, }); ``` #### GET strapi.documents().findMany() — $between **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { rating: { $between: [1, 20], }, }, }); ``` #### GET strapi.documents().findMany() — $contains Attribute contains the input value (case-sensitive). **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $contains: 'Hello', }, }, }); ``` #### GET strapi.documents().findMany() — $notContains Attribute does not contain the input value (case-sensitive). **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $notContains: 'Hello', }, }, }); ``` #### GET strapi.documents().findMany() — $containsi **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $containsi: 'hello', }, }, }); ``` #### GET strapi.documents().findMany() — $notContainsi **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $notContainsi: 'hello', }, }, }); ``` #### GET strapi.documents().findMany() — $startsWith Attribute starts with input value (case-sensitive). **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $startsWith: 'ABCD', }, }, }); ``` #### GET strapi.documents().findMany() — $startsWithi Attribute starts with input value (case-insensitive). **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $startsWithi: 'ABCD', // will return the same as filtering with 'abcd' }, }, }); ``` #### GET strapi.documents().findMany() — $endsWith Attribute ends with input value (case-sensitive). **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $endsWith: 'ABCD', }, }, }); ``` #### GET strapi.documents().findMany() — $endsWithi Attribute ends with input value (case-insensitive). **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $endsWith: 'ABCD', // will return the same as filtering with 'abcd' }, }, }); ``` #### GET strapi.documents().findMany() — $null **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $null: true, }, }, }); ``` #### GET strapi.documents().findMany() — $notNull **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { title: { $notNull: true, }, }, }); ``` ## Logical operators #### GET strapi.documents().findMany() — $and **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { $and: [ { title: 'Hello World', }, { createdAt: { $gt: '2021-11-17T14:28:25.843Z' }, }, ], }, }); ``` **Implicit $and:** ``` // $and will be used implicitly when passing an object with nested conditions: const entries = await strapi.documents('api::article.article').findMany({ filters: { title: 'Hello World', createdAt: { $gt: '2021-11-17T14:28:25.843Z' }, }, }); ``` #### GET strapi.documents().findMany() — $or **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { $or: [ { title: 'Hello World', }, { createdAt: { $gt: '2021-11-17T14:28:25.843Z' }, }, ], }, }); ``` #### GET strapi.documents().findMany() — $not Negates the nested conditions. **JavaScript:** ``` const entries = await strapi.documents('api::article.article').findMany({ filters: { $not: { title: 'Hello World', }, }, }); ``` :::note `$not` can be used as: - a logical operator (e.g. in `filters: { $not: { // conditions... }}`) - [an attribute operator](#not) (e.g. in `filters: { attribute-name: $not: { ... } }`). ::: :::tip `$and`, `$or` and `$not` operators are nestable inside of another `$and`, `$or` or `$not` operator. ::: # Using the locale parameter with the Document Service API Source: https://docs.strapi.io/cms/api/document-service/locale # Document Service API: Using the `locale` parameter The `locale` parameter in the Document Service API lets you query, create, update, delete, publish, and unpublish documents for specific language versions using methods like `findOne()`, `findMany()`, `update()`, and `delete()`. By default the [Document Service API](/cms/api/document-service) returns the default locale version of documents (which is 'en', i.e. the English version, unless another default locale has been set for the application, see [Internationalization (i18n) feature](/cms/features/internationalization)). This page describes how to use the `locale` parameter to get or manipulate data only for specific locales. ## Get a locale version with `findOne()` {#find-one} #### GET strapi.documents().findOne() — Get a locale version with findOne() Pass a locale to findOne() to get the version of the document for that locale. **JavaScript:** ``` await strapi.documents('api::restaurant.restaurant').findOne({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', locale: 'fr', }); ``` **Response 200 OK:** ```json { documentId: "a1b2c3d4e5f6g7h8i9j0klm", name: "Biscotte Restaurant", publishedAt: null, // draft version (default) locale: "fr", // as asked from the parameters // … } ``` If no `status` parameter is passed, the `draft` version is returned by default. ## Get a locale version with `findFirst()` {#find-first} #### GET strapi.documents().findFirst() — Get a locale version with findFirst() Pass a locale to findFirst() to return documents matching that locale. **JavaScript:** ``` const document = await strapi.documents('api::article.article').findFirst({ locale: 'fr', }); ``` **Response 200 OK:** ```json { "documentId": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article" // … } ``` If no `status` parameter is passed, the `draft` version is returned by default. ## Get locale versions with `findMany()` {#find-many} If no `status` parameter is passed, the `draft` versions are returned by default. #### GET strapi.documents().findMany() — Get locale versions with findMany() Pass a locale to findMany() to return all documents that have this locale available. **JavaScript:** ``` // Defaults to status: draft await strapi.documents('api::restaurant.restaurant').findMany({ locale: 'fr' }); ``` **Response 200 OK:** ```json [ { documentId: 'a1b2c3d4e5f6g7h8i9j0klm', name: 'Restaurant Biscotte', publishedAt: null, locale: 'fr', // … }, // … ] ```
Explanation: Given the following 4 documents that have various locales: - Document A: - en - `fr` - it - Document B: - en - it - Document C: - `fr` - Document D: - `fr` - it `findMany({ locale: 'fr' })` would only return the draft version of the documents that have a `'fr'` locale version, that is documents A, C, and D.
## `create()` a document for a locale {#create} #### GET strapi.documents().create() — Create a document for a locale Pass a locale to create() to create the document for that specific locale. **JavaScript:** ``` await strapi.documents('api::restaurant.restaurant').create({ locale: 'es' // if not passed, the draft is created for the default locale data: { name: 'Restaurante B' } }) ``` **Response 200 OK:** ```json { documentId: "pw2s0nh5ub1zmnk0d80vgqrh", name: "Restaurante B", publishedAt: null, locale: "es" // … } ``` ## `update()` a locale version {#update} #### GET strapi.documents().update() — Update a locale version Pass a locale to update() to update only that specific locale version of a document. **JavaScript:** ``` await strapi.documents('api::restaurant.restaurant').update({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', locale: 'es', data: { name: 'Nuevo nombre del restaurante' }, }); ``` **Response 200 OK:** ```json { documentId: "a1b2c3d4e5f6g7h8i9j0klm", name: "Nuevo nombre del restaurante", locale: "es", publishedAt: null, // … } ``` ## `delete()` locale versions {#delete} Use the `locale` parameter with the [`delete()` method](/cms/api/document-service#delete) of the Document Service API to delete only some locales. Unless a specific `status` parameter is passed, this deletes both the draft and published versions. ### Delete a locale version #### GET strapi.documents().delete() — Delete a locale version Pass a locale to delete() to delete only that specific locale version of a document. **JavaScript:** ``` await strapi.documents('api::restaurant.restaurant').delete({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', // documentId, locale: 'es', }); ``` ### Delete all locale versions #### GET strapi.documents().delete() — Delete all locale versions Use the * wildcard with the locale parameter to delete all locale versions of a document. **JavaScript:** ``` await strapi.documents('api::restaurant.restaurant').delete({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', // documentId, locale: '*', }); // for all existing locales ``` **Response 200 OK:** ```json { "documentId": "a1b2c3d4e5f6g7h8i9j0klm", // All of the deleted locale versions are returned "versions": [ { "title": "Test Article" } ] } ``` ## `publish()` locale versions {#publish} To publish only specific locale versions of a document with the [`publish()` method](/cms/api/document-service#publish) of the Document Service API, pass `locale` as a parameter: ### Publish a locale version #### GET strapi.documents().publish() — Publish a locale version Pass a locale to publish() to publish only that specific locale version of a document. **JavaScript:** ``` await strapi.documents('api::restaurant.restaurant').publish({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', locale: 'fr', }); ``` **Response 200 OK:** ```json { versions: [ { documentId: 'a1b2c3d4e5f6g7h8i9j0klm', name: 'Restaurant Biscotte', publishedAt: '2024-03-14T18:38:05.674Z', locale: 'fr', // … }, ] } ``` ### Publish all locale versions #### GET strapi.documents().publish() — Publish all locale versions Use the * wildcard with the locale parameter to publish all locale versions of a document. **JavaScript:** ``` await strapi .documents('api::restaurant.restaurant') .publish({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', locale: '*' }); ``` **Response 200 OK:** ```json { "versions": [ { "documentId": "a1b2c3d4e5f6g7h8i9j0klm", "publishedAt": "2024-03-14T18:45:21.857Z", "locale": "en" // … }, { "documentId": "a1b2c3d4e5f6g7h8i9j0klm", "publishedAt": "2024-03-14T18:45:21.857Z", "locale": "es" // … }, { "documentId": "a1b2c3d4e5f6g7h8i9j0klm", "publishedAt": "2024-03-14T18:45:21.857Z", "locale": "fr" // … } ] } ``` ## `unpublish()` locale versions {#unpublish} To publish only specific locale versions of a document with the [`unpublish()` method](/cms/api/document-service#unpublish) of the Document Service API, pass `locale` as a parameter: ### Unpublish a locale version #### GET strapi.documents().unpublish() — Unpublish a locale version Pass a locale to unpublish() to unpublish only that specific locale version of a document. **JavaScript:** ``` await strapi .documents('api::restaurant.restaurant') .unpublish({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', locale: 'fr' }); ``` **Response 200 OK:** ```json { versions: 1 } ``` ### Unpublish all locale versions #### GET strapi.documents().unpublish() — Unpublish all locale versions Use the * wildcard with the locale parameter to unpublish all locale versions of a document. **JavaScript:** ``` await strapi .documents('api::restaurant.restaurant') .unpublish({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', locale: '*' }); ``` **Response 200 OK:** ```json { versions: 3 } ``` #### GET strapi.documents().unpublish() — Unpublish with fields selection Unpublish a document while selecting specific fields to return. **JavaScript:** ``` const document = await strapi.documents('api::article.article').unpublish({ documentId: 'cjld2cjxh0000qzrmn831i7rn', fields: ['title'], }); ``` **Response 200 OK:** ```json { "documentId": "cjld2cjxh0000qzrmn831i7rn", // All of the unpublished locale versions are returned "versions": [ { "title": "Test Article" } ] } ``` ## `discardDraft()` for locale versions {#discard-draft} To discard draft data only for some locales versions of a document with the [`discardDraft()` method](/cms/api/document-service#discarddraft) of the Document Service API, pass `locale` as a parameter: ### Discard draft for a locale version #### GET strapi.documents().discardDraft() — Discard draft for a locale version Pass a locale to discardDraft() to discard draft data for that specific locale version. **JavaScript:** ``` await strapi .documents('api::restaurant.restaurant') .discardDraft({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', locale: 'fr' }); ``` **Response 200 OK:** ```json { versions: [ { documentId: 'a1b2c3d4e5f6g7h8i9j0klm', name: 'Restaurant Biscotte', publishedAt: null, locale: 'fr', // … }, ] } ``` ### Discard drafts for all locale versions #### GET strapi.documents().discardDraft() — Discard drafts for all locale versions Use the * wildcard with the locale parameter to discard drafts for all locale versions of a document. **JavaScript:** ``` await strapi .documents('api::restaurant.restaurant') .discardDraft({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', locale: '*' }); ``` **Response 200 OK:** ```json { versions: [ { documentId: 'a1b2c3d4e5f6g7h8i9j0klm', name: 'Biscotte Restaurant', publishedAt: null, locale: 'en', // … }, { documentId: 'a1b2c3d4e5f6g7h8i9j0klm', name: 'Restaurant Biscotte', publishedAt: null, locale: 'fr', // … }, { documentId: 'a1b2c3d4e5f6g7h8i9j0klm', name: 'Biscotte Restaurante', publishedAt: null, locale: 'es', // … }, ] } ``` ## `count()` documents for a locale {#count} To count documents for a specific locale, pass the `locale` along with other parameters to the [`count()` method](/cms/api/document-service#count) of the Document Service API. If no `status` parameter is passed, draft documents are counted (which is the total of available documents for the locale since even published documents are counted as having a draft version): ```js // Count number of published documents in French strapi.documents('api::restaurant.restaurant').count({ locale: 'fr' }); ``` # Extending the Document Service behavior Source: https://docs.strapi.io/cms/api/document-service/middlewares # Document Service API: Middlewares Document Service middlewares allow you to perform actions before and after Document Service methods run by registering middleware functions via `strapi.documents.use()` with access to content type context and method parameters. The [Document Service API](/cms/api/document-service) offers the ability to extend its behavior thanks to middlewares. Document Service middlewares allow you to perform actions before and/or after a method runs.
Simplified Strapi backend diagram with controllers highlighted
The diagram represents a simplified version of how a request travels through the Strapi back end, with the Document Service highlighted. The backend customization introduction page includes a complete, interactive diagram.
## Registering a middleware Syntax: `strapi.documents.use(middleware)` ### Parameters A middleware is a function that receives a context and a next function. Syntax: `(context, next) => ReturnType` | Parameter | Description | Type | |-----------|---------------------------------------|------------| | `context` | Middleware context | `Context` | | `next` | Call the next middleware in the stack | `function` | #### `context` | Parameter | Description | Type | |---------------|--------------------------------------------------------------------------------------|---------------| | `action` | The method that is running ([see available methods](/cms/api/document-service)) | `string` | | `params` | The method params ([see available methods](/cms/api/document-service)) | `Object` | | `uid` | Content type unique identifier | `string` | | `contentType` | Content type | `ContentType` |
Examples: The following examples show what `context` might include depending on the method called: ```js { uid: "api::restaurant.restaurant", contentType: { kind: "collectionType", collectionName: "restaurants", info: { singularName: "restaurant", pluralName: "restaurants", displayName: "restaurant" }, options: { draftAndPublish: true }, pluginOptions: {}, attributes: { name: { /*...*/ }, description: { /*...*/ }, createdAt: { /*...*/ }, updatedAt: { /*...*/ }, publishedAt: { /*...*/ }, createdBy: { /*...*/ }, updatedBy: { /*...*/ }, locale: { /*...*/ }, }, apiName: "restaurant", globalId: "Restaurants", uid: "api::restaurant.restaurant", modelType: "contentType", modelName: "restaurant", actions: { /*...*/ }, lifecycles: { /*...*/ }, }, action: "findOne", params: { documentId: 'hp7hjvrbt8rcgkmabntu0aoq', locale: undefined, status: "publish" populate: { /*...*/ }, } } ``` ```js { uid: "api::restaurant.restaurant", contentType: { kind: "collectionType", collectionName: "restaurants", info: { singularName: "restaurant", pluralName: "restaurants", displayName: "restaurant" }, options: { draftAndPublish: true }, pluginOptions: {}, attributes: { name: { /*...*/ }, description: { /*...*/ }, createdAt: { /*...*/ }, updatedAt: { /*...*/ }, publishedAt: { /*...*/ }, createdBy: { /*...*/ }, updatedBy: { /*...*/ }, locale: { /*...*/ }, }, apiName: "restaurant", globalId: "Restaurants", uid: "api::restaurant.restaurant", modelType: "contentType", modelName: "restaurant", actions: { /*...*/ }, lifecycles: { /*...*/ }, }, action: "findMany", params: { filters: { /*...*/ }, status: "draft", locale: null, fields: ['name', 'description'], } } ``` ```js { uid: "api::restaurant.restaurant", contentType: { kind: "collectionType", collectionName: "restaurants", info: { singularName: "restaurant", pluralName: "restaurants", displayName: "restaurant" }, options: { draftAndPublish: true }, pluginOptions: {}, attributes: { name: { /*...*/ }, description: { /*...*/ }, createdAt: { /*...*/ }, updatedAt: { /*...*/ }, publishedAt: { /*...*/ }, createdBy: { /*...*/ }, updatedBy: { /*...*/ }, locale: { /*...*/ }, }, apiName: "restaurant", globalId: "Restaurants", uid: "api::restaurant.restaurant", modelType: "contentType", modelName: "restaurant", actions: { /*...*/ }, lifecycles: { /*...*/ }, }, action: "create", params: { data: { /*...*/ }, status: "draft", populate: { /*...*/ }, } } ``` ```js { uid: "api::restaurant.restaurant", contentType: { kind: "collectionType", collectionName: "restaurants", info: { singularName: "restaurant", pluralName: "restaurants", displayName: "restaurant" }, options: { draftAndPublish: true }, pluginOptions: {}, attributes: { name: { /*...*/ }, description: { /*...*/ }, createdAt: { /*...*/ }, updatedAt: { /*...*/ }, publishedAt: { /*...*/ }, createdBy: { /*...*/ }, updatedBy: { /*...*/ }, locale: { /*...*/ }, }, apiName: "restaurant", globalId: "Restaurants", uid: "api::restaurant.restaurant", modelType: "contentType", modelName: "restaurant", actions: { /*...*/ }, lifecycles: { /*...*/ }, }, action: "update", params: { data: { /*...*/ }, documentId: 'hp7hjvrbt8rcgkmabntu0aoq', locale: undefined, status: "draft" populate: { /*...*/ }, } } ``` ```js { uid: "api::restaurant.restaurant", contentType: { kind: "collectionType", collectionName: "restaurants", info: { singularName: "restaurant", pluralName: "restaurants", displayName: "restaurant" }, options: { draftAndPublish: true }, pluginOptions: {}, attributes: { name: { /*...*/ }, description: { /*...*/ }, createdAt: { /*...*/ }, updatedAt: { /*...*/ }, publishedAt: { /*...*/ }, createdBy: { /*...*/ }, updatedBy: { /*...*/ }, locale: { /*...*/ }, }, apiName: "restaurant", globalId: "Restaurants", uid: "api::restaurant.restaurant", modelType: "contentType", modelName: "restaurant", actions: { /*...*/ }, lifecycles: { /*...*/ }, }, action: "delete", params: { data: { /*...*/ }, documentId: 'hp7hjvrbt8rcgkmabntu0aoq', locale: "*", populate: { /*...*/ }, } } ```
#### `next` `next` is a function without parameters that calls the next middleware in the stack and return its response. **Example** ```js strapi.documents.use((context, next) => { return next(); }); ``` ### Where to register Generaly speaking you should register your middlewares during the Strapi registration phase. #### Users The middleware must be registered in the general `register()` lifecycle method: ```js title="/src/index.js|ts" module.exports = { register({ strapi }) { strapi.documents.use((context, next) => { // your logic return next(); }); }, // bootstrap({ strapi }) {}, // destroy({ strapi }) {}, }; ``` #### Plugin developers The middleware must be registered in the plugin's `register()` lifecycle method: ```js title="/(plugin-root-folder)/strapi-server.js|ts" module.exports = { register({ strapi }) { strapi.documents.use((context, next) => { // your logic return next(); }); }, // bootstrap({ strapi }) {}, // destroy({ strapi }) {}, }; ``` ## Implementing a middleware When implementing a middleware, always return the response from `next()`. Failing to do this will break the Strapi application. ### Examples ```js const applyTo = ['api::article.article']; strapi.documents.use((context, next) => { // Only run for certain content types if (!applyTo.includes(context.uid)) { return next(); } // Only run for certain actions if (['create', 'update'].includes(context.action)) { context.params.data.fullName = `${context.params.data.firstName} ${context.params.data.lastName}`; } const result = await next(); // do something with the result before returning it return result }); ```
:::strapi Lifecycle hooks The Document Service API triggers various database lifecycle hooks based on which method is called. For a complete reference, see [Document Service API: Lifecycle hooks](/cms/migration/v4-to-v5/breaking-changes/lifecycle-hooks-document-service#table). ::: # Using Populate with the Document Service API Source: https://docs.strapi.io/cms/api/document-service/populate # Document Service API: Populating fields Use the `populate` parameter with the Document Service API to explicitly load relations, media fields, components, and dynamic zones at one or multiple levels deep, and within `create()`, `update()`, `publish()`, and `delete()` operations. By default the [Document Service API](/cms/api/document-service) does not populate any relations, media fields, components, or dynamic zones. This page describes how to use the `populate` parameter to populate specific fields. :::tip You can also use the `select` parameter to return only specific fields with the query results (see the [`select` parameter](/cms/api/document-service/fields) documentation). ::: :::caution If the Users & Permissions plugin is installed, the `find` permission must be enabled for the content-types that are being populated. If a role doesn't have access to a content-type it will not be populated. ::: ## Relations and media fields Queries can accept a `populate` parameter to explicitly define which fields to populate, with the following syntax option examples. This includes all relation types: one-to-many, many-to-one, many-to-many, and polymorphic relations (morphToOne, morphToMany). ### Populate 1 level for all relations #### GET strapi.documents( — Populate 1 level for all relations Populate one-level deep for all relations using the wildcard. **JavaScript:** ``` const documents = await strapi.documents("api::article.article").findMany({ populate: "*", }); ``` **Response 200 OK:** ```json { [ { "id": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article", "slug": "test-article", "body": "Test 1", // ... "headerImage": { "data": { "id": 1, "attributes": { "name": "17520.jpg", "alternativeText": "17520.jpg", "formats": { // ... } // ... } } }, "author": { // ... }, "categories": { // ... } } // ... ] } ``` ### Populate 1 level for specific relations #### GET strapi.documents( — Populate 1 level for specific relations Populate specific relations one-level deep using an array. **JavaScript:** ``` const documents = await strapi.documents("api::article.article").findMany({ populate: ["headerImage"], }); ``` **Response 200 OK:** ```json [ { "id": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article", "slug": "test-article", "body": "Test 1", // ... "headerImage": { "id": 2, "name": "17520.jpg" // ... } } // ... ] ``` ### Populate several levels deep for specific relations #### GET strapi.documents( — Populate several levels deep for specific relations Populate specific relations several levels deep using nested populate. **JavaScript:** ``` const documents = await strapi.documents("api::article.article").findMany({ populate: { categories: { populate: ["articles"], }, }, }); ``` **Response 200 OK:** ```json [ { "id": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article", "slug": "test-article", "body": "Test 1", // ... "categories": { "id": 1, "name": "Test Category", "slug": "test-category", "description": "Test 1" // ... "articles": [ { "id": 1, "title": "Test Article", "slug": "test-article", "body": "Test 1", // ... } // ... ] } } // ... ] ``` ### Sort populated relations Use the `sort` parameter inside a `populate` object to order related entries by an attribute. For many-to-many and other join-table relations, an explicit `sort` takes precedence over the default connect order. #### GET strapi.documents( — Sort populated relations Order related entries by an attribute using the sort parameter inside a populate object. **JavaScript:** ``` const documents = await strapi.documents("api::article.article").findMany({ populate: { categories: { sort: 'name:asc', }, }, }); ``` **Response 200 OK:** ```json [ { "id": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article", // ... "categories": [ { "id": 1, "name": "Architecture" // ... }, { "id": 3, "name": "Technology" // ... } ] } // ... ] ``` :::note Omit `sort` from a `populate` object to preserve the default connect order (the order in which entries were associated). ::: ## Components & Dynamic Zones Components are populated the same way as relations: #### GET strapi.documents( — Populate components Populate components using the same syntax as relations. **JavaScript:** ``` const documents = await strapi.documents("api::article.article").findMany({ populate: ["testComp"], }); ``` **Response 200 OK:** ```json [ { "id": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article", "slug": "test-article", "body": "Test 1", // ... "testComp": { "id": 1, "name": "Test Component" // ... } } // ... ] ``` Dynamic zones are highly dynamic content structures by essence. Standard populate queries (like `populate: '*'` or `populate: ['testDZ']`) will only retrieve the default, non-relational scalar fields (e.g., strings, numbers) of components within a dynamic zone. They will **not** automatically fetch nested relations, media fields, or nested components. To populate component-specific nested relations, media fields, or components within a dynamic zone, you must define per-component populate queries using the `on` property (fragment population syntax). #### GET strapi.documents( — Populate dynamic zones Populate dynamic zones using per-component queries with the on property. **JavaScript:** ``` const documents = await strapi.documents("api::article.article").findMany({ populate: { testDZ: { on: { "test.test-compo": { fields: ["testString"], populate: ["testNestedCompo"], }, }, }, }, }); ``` **Response 200 OK:** ```json [ { "id": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article", "slug": "test-article", "body": "Test 1", // ... "testDZ": [ { "id": 3, "__component": "test.test-compo", "testString": "test1", "testNestedCompo": { "id": 3, "testNestedString": "testNested1" } } ] } // ... ] ``` ## Populating with `create()` #### GET strapi.documents( — Populate with create Populate relations in the response when creating a document. **JavaScript:** ``` strapi.documents("api::article.article").create({ data: { title: "Test Article", slug: "test-article", body: "Test 1", headerImage: 2, }, populate: ["headerImage"], }); ``` **Response 200 OK:** ```json { "id": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article", "slug": "test-article", "body": "Test 1", "headerImage": { "id": 2, "name": "17520.jpg" // ... } } ``` ## Populating with `update()` #### GET strapi.documents( — Populate with update Populate relations in the response when updating a document. **JavaScript:** ``` strapi.documents("api::article.article").update({ documentId: "cjld2cjxh0000qzrmn831i7rn", data: { title: "Test Article Update", }, populate: ["headerImage"], }); ``` **Response 200 OK:** ```json { "id": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article Update", "slug": "test-article", "body": "Test 1", "headerImage": { "id": 2, "name": "17520.jpg" // ... } } ``` ## Populating with `publish()` Same behavior applies with `unpublish()` and `discardDraft()`. #### GET strapi.documents( — Populate with publish Populate relations in the response when publishing a document. **JavaScript:** ``` strapi.documents("api::article.article").publish({ documentId: "cjld2cjxh0000qzrmn831i7rn", populate: ["headerImage"], }); ``` **Response 200 OK:** ```json { "id": "cjld2cjxh0000qzrmn831i7rn", "versions": [ { "id": "cjld2cjxh0001qzrm1q1i7rn", "locale": "en", // ... "headerImage": { "id": 2, "name": "17520.jpg" // ... } } ] } ``` ## Populating with `delete()` To populate while deleting documents: #### GET strapi.documents( — Populate with delete Populate relations in the response when deleting a document. **JavaScript:** ``` strapi.documents("api::article.article").delete({ documentId: "cjld2cjxh0000qzrmn831i7rn", populate: ["headerImage"], }); ``` **Response 200 OK:** ```json { "documentId": "cjld2cjxh0000qzrmn831i7rn", "entries": [ { "id": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article", "slug": "test-article", "body": "Test 1", "headerImage": { "id": 2, "name": "17520.jpg" // ... } // ... } ] } ``` # Using publicationFilter with the Document Service API Source: https://docs.strapi.io/cms/api/document-service/publication-filter # Document Service API: `publicationFilter` The [`status`](/cms/api/document-service/status) parameter selects which **row slice** to read for each document: `draft` rows have `publishedAt: null`, and `published` rows have a non-null `publishedAt`. The optional `publicationFilter` parameter selects a **derived publication cohort** first: a set of `(documentId, locale)` pairs (or `documentId` only when [Internationalization (i18n)](/cms/features/internationalization) is disabled) defined by how draft and published rows relate. Strapi then returns the row that matches both the cohort and the resolved `status`. :::prerequisites The [Draft & Publish](/cms/features/draft-and-publish) feature must be enabled on the content-type. If Draft & Publish is disabled, `publicationFilter` has no effect. ::: `publicationFilter` is supported on `findOne()`, `findFirst()`, `findMany()`, and `count()`. It can be combined with [`filters`](/cms/api/document-service/filters), [`populate`](/cms/api/document-service/populate), and other query parameters. Invalid values raise a validation error. ## Default `status` when `publicationFilter` is used {#default-status} `publicationFilter` is applied **after** `status` is resolved (explicitly or by default). Defaults differ by API surface: | API surface | Default `status` when omitted | | ----------- | ----------------------------- | | Document Service API (direct) | `'draft'` | | [REST API](/cms/api/rest/publication-filter) | `'published'` | | [GraphQL API](/cms/api/graphql#publication-filter) | `PUBLISHED` | The following example compares Document Service and REST behavior when only `publicationFilter: 'modified'` is passed: ```js // Document Service API → draft rows in the modified cohort await strapi.documents('api::restaurant.restaurant').findMany({ publicationFilter: 'modified', }); // REST: GET /api/restaurants?publicationFilter=modified → published rows in the modified cohort ``` Pair-scoped modes such as `never-published` only include draft rows in the cohort. With REST or GraphQL defaults (`status=published`), those queries return an empty result set unless you pass `status=draft` / `status: DRAFT`. ## Available values {#values} REST and the Document Service API use kebab-case strings. GraphQL exposes the same cohorts through the [`PublicationFilter` enum](/cms/api/graphql#publication-filter). | Value | Scope | Cohort definition (which `(documentId, locale)` pairs match) | | ----- | ----- | -------------------------------------------------------------- | | `never-published` | Pair | No row with non-null `publishedAt` exists for the same `(documentId, locale)` | | `has-published-version` | Pair | **Both** a draft row and a published row exist for the same `(documentId, locale)` | | `modified` | Pair | Both slices exist and `draft.updatedAt > published.updatedAt` | | `unmodified` | Pair | Both slices exist and `draft.updatedAt <= published.updatedAt` | | `never-published-document` | Document | No row with non-null `publishedAt` exists for the same `documentId` in **any** locale | | `has-published-version-document` | Document | At least one published row exists for the same `documentId` in **any** locale | | `published-without-draft` | Pair | A published row exists for the pair and **no** draft row exists for the same `(documentId, locale)` | | `published-with-draft` | Pair | A published row exists for the pair and a draft row **also** exists for the same `(documentId, locale)` | For content-types without i18n, read `(documentId, locale)` as `documentId` only. ### Semantics notes {#semantics} - **`has-published-version` excludes orphan published rows**: If only a published row exists for a pair (no draft sibling), that pair is **not** in the `has-published-version` cohort. Orphan published rows can appear under `published-without-draft` when querying with `status: 'published'`. - **`modified` / `unmodified` require both slices**: Pairs with only a draft or only a published row are not included. - **`modified` ∪ `unmodified` = `has-published-version`** (for the same `status`): The two modes partition pairs that have both slices. - **Document-scoped modes**: Existence checks use `documentId` only. A document with draft EN + published NL qualifies for `has-published-version-document` even though EN is never published at the pair level. - **Published-slice diagnostics** (`published-without-draft`, `published-with-draft`): Only select published rows. They return no rows when `status` is `'draft'`. ### Content Manager list filters {#content-manager} The Content Manager **Status** filter (`__status`) is translated server-side. Only the **Draft (never published)** option uses `publicationFilter`: | Content Manager filter | Document Service query equivalent | | ---------------------- | --------------------------------- | | Draft (never published) | `status: 'draft'`, `publicationFilter: 'never-published-document'` | | Published (all) | `status: 'published'` (no `publicationFilter`) | | Published (modified) | Internal `publicationStatusFilter` (not a public REST/GraphQL parameter); similar intent to `status: 'published'` + `publicationFilter: 'modified'` but implemented separately in the Content Manager API | | Published (unmodified) | Internal `publicationStatusFilter` (not a public REST/GraphQL parameter) | The **Draft (never published)** filter is document-scoped (`never-published-document`), not pair-scoped `never-published`. ## Combine `status` and `publicationFilter` {#status-combination} | `status` | `publicationFilter` | Rows returned | | -------- | ------------------- | ------------- | | `draft` | `never-published` | Draft rows for pairs never published in that locale | | `published` | `never-published` | Empty | | `draft` | `has-published-version` | Draft rows for pairs that also have a published version | | `published` | `has-published-version` | Published rows for pairs that also have a draft version (excludes orphan published-only pairs) | | `draft` | `modified` | Draft rows newer than their published peer | | `published` | `modified` | Published rows whose draft peer is newer | | `draft` | `unmodified` | Draft rows not newer than their published peer | | `published` | `unmodified` | Published rows whose draft peer is not newer | | `draft` | `never-published-document` | Draft rows whose document has no published row in any locale | | `published` | `never-published-document` | Empty | | `draft` | `has-published-version-document` | Draft rows whose document has at least one published row (any locale) | | `published` | `has-published-version-document` | Published rows whose document has at least one draft row (any locale) | | `published` | `published-without-draft` | Published rows with no draft sibling for the same pair | | `draft` | `published-without-draft` | Empty | | `published` | `published-with-draft` | Published rows that have a draft sibling for the same pair | | `draft` | `published-with-draft` | Empty | :::note Valid but empty combinations do not return validation errors. ::: ## Query never-published drafts {#never-published} Return draft rows for `(documentId, locale)` pairs with no published version for that locale: ```js const documents = await strapi.documents('api::restaurant.restaurant').findMany({ status: 'draft', publicationFilter: 'never-published', }); ``` ## Query has-published-version drafts {#has-published-version} Return draft rows where a published row also exists for the same `(documentId, locale)`. Orphan published-only pairs are excluded: ```js const documents = await strapi.documents('api::restaurant.restaurant').findMany({ status: 'draft', publicationFilter: 'has-published-version', }); ``` ## Query modified or unmodified documents {#modified-unmodified} Compare `updatedAt` on the draft and published rows for the same pair: ```js // Draft side of modified pairs await strapi.documents('api::restaurant.restaurant').findMany({ status: 'draft', publicationFilter: 'modified', }); // Published side of unmodified pairs await strapi.documents('api::restaurant.restaurant').findMany({ status: 'published', publicationFilter: 'unmodified', }); ``` ## Query document-scoped cohorts {#document-scoped} Return draft rows for documents that have never been published in any locale: ```js await strapi.documents('api::restaurant.restaurant').findMany({ status: 'draft', publicationFilter: 'never-published-document', }); ``` A multi-locale document with one published locale is excluded entirely, including its draft-only locales. Return draft rows for documents that have at least one published row in any locale: ```js await strapi.documents('api::restaurant.restaurant').findMany({ status: 'draft', publicationFilter: 'has-published-version-document', }); ``` This is broader than pair-scoped `has-published-version`. ## Query published rows without or with a draft peer {#published-slice} `published-without-draft` and `published-with-draft` partition published rows per `(documentId, locale)` (excluding pairs with no published row): ```js // Orphan published rows (published row, no draft sibling for the same pair) await strapi.documents('api::restaurant.restaurant').findMany({ status: 'published', publicationFilter: 'published-without-draft', }); // Published rows that still have a draft sibling await strapi.documents('api::restaurant.restaurant').findMany({ status: 'published', publicationFilter: 'published-with-draft', }); ``` ## Use with `findOne()` and `findFirst()` {#find-one-find-first} `publicationFilter` applies the same cohort rules. If the requested document (and locale, when applicable) is not in the cohort, `findOne()` and `findFirst()` return `null` even when the `documentId` exists: ```js await strapi.documents('api::restaurant.restaurant').findOne({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', status: 'draft', publicationFilter: 'never-published', }); ``` ## Combine with `filters` and `populate` {#filters-populate} `publicationFilter` is merged with other query filters (logical AND). When [populating relations](/cms/api/document-service/populate), nested queries on draft & publish content-types inherit the same cohort logic so populated results stay consistent with the parent query. ## Count documents in a cohort {#count} Count draft rows in the never-published cohort: ```js const neverPublishedCount = await strapi .documents('api::restaurant.restaurant') .count({ status: 'draft', publicationFilter: 'never-published', }); ``` Without `publicationFilter`, `count({ status: 'draft' })` still counts every draft row, including drafts whose document already has a published version. Use `publicationFilter: 'never-published'` or `'never-published-document'` to count only never-published cohorts (see [`status` documentation](/cms/api/document-service/status#count)). ## Validation {#validation} Unknown `publicationFilter` values are rejected: - Document Service API: throws a validation error. - REST API: returns HTTP `400`. - GraphQL: invalid enum values fail at query validation. ## Deprecated `hasPublishedVersion` parameter {#has-published-version-deprecated} The boolean `hasPublishedVersion` parameter is deprecated in favor of `publicationFilter`. Strapi still accepts it on the REST API, GraphQL, and Document Service API and maps it to **document-scoped** modes: | `hasPublishedVersion` | Maps to | | --------------------- | ------- | | `false` (or string `'false'`) | `never-published-document` | | `true` (or string `'true'`) | `has-published-version-document` | If both `publicationFilter` and `hasPublishedVersion` are passed, `publicationFilter` takes precedence. REST and GraphQL examples: [REST API: `publicationFilter`](/cms/api/rest/publication-filter#has-published-version-deprecated), [GraphQL API: `publicationFilter`](/cms/api/graphql#publication-filter). ## Why not filter on `publishedAt` alone? {#why-not-published-at} A single row's `publishedAt` only describes that row. Cohorts such as `never-published`, `has-published-version`, and `modified` require comparing or correlating **two rows** for the same `(documentId, locale)`. `publicationFilter` encodes those rules in one server-side query instead of multiple client round-trips. # Using Sort & Pagination with the Document Service API Source: https://docs.strapi.io/cms/api/document-service/sort-pagination # Document Service API: Sorting and paginating results Use the Document Service API's `sort` and pagination parameters to order query results by single or multiple fields and control result limits with `limit` and `start`. The [Document Service API](/cms/api/document-service) offers the ability to sort and paginate query results. ## Sort To sort results returned by the Document Service API, include the `sort` parameter with queries. ### Sort on a single field #### GET strapi.documents().findMany() — Sort on a single field Sort results based on a single field using a string value. **JavaScript:** ``` const documents = await strapi.documents("api::article.article").findMany({ sort: "title:asc", }); ``` **Response 200 OK:** ```json [ { "documentId": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article", "slug": "test-article", "body": "Test 1" }, { "documentId": "cjld2cjxh0001qzrm5q1j5q7m", "title": "Test Article 2", "slug": "test-article-2", "body": "Test 2" } ] ``` ### Sort on multiple fields #### GET strapi.documents().findMany() — Sort on multiple fields Sort results on multiple fields by passing an array of sort objects. **JavaScript:** ``` const documents = await strapi.documents("api::article.article").findMany({ sort: [{ title: "asc" }, { slug: "desc" }], }); ``` **Response 200 OK:** ```json [ { "documentId": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article", "slug": "test-article", "body": "Test 1" }, { "documentId": "cjld2cjxh0001qzrm5q1j5q7m", "title": "Test Article 2", "slug": "test-article-2", "body": "Test 2" } ] ``` ## Pagination #### GET strapi.documents().findMany() — Pagination Paginate results using the limit and start parameters. **JavaScript:** ``` const documents = await strapi.documents("api::article.article").findMany({ limit: 10, start: 0, }); ``` **Response 200 OK:** ```json [ { "documentId": "cjld2cjxh0000qzrmn831i7rn", "title": "Test Article", "slug": "test-article", "body": "Test 1" }, { "documentId": "cjld2cjxh0001qzrm5q1j5q7m", "title": "Test Article 2", "slug": "test-article-2", "body": "Test 2" } ] ``` # Using Draft & Publish with the Document Service API Source: https://docs.strapi.io/cms/api/document-service/status # Document Service API: Usage with Draft & Publish Use the `status` parameter with the Document Service API to retrieve published or draft versions of documents, count documents by status, and directly publish documents during creation or updates. By default the [Document Service API](/cms/api/document-service) returns the draft version of a document when the [Draft & Publish](/cms/features/draft-and-publish) feature is enabled. This page describes how to use the `status` parameter to: - return the published version of a document, - count documents depending on their status, - and directly publish a document while creating it or updating it. :::note Passing `{ status: 'draft' }` to a Document Service API query returns the same results as not passing any `status` parameter. ::: For derived publication cohorts (never-published, modified, and others), see [Document Service API: `publicationFilter`](/cms/api/document-service/publication-filter). ## Get the published version with `findOne()` {#find-one} #### GET strapi.documents().findOne() — findOne() with status: Return the published version of a specific document. **JavaScript:** ``` await strapi.documents('api::restaurant.restaurant').findOne({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', status: 'published' }); ``` **Response 200 OK:** ```json { documentId: "a1b2c3d4e5f6g7h8i9j0klm", name: "Biscotte Restaurant", publishedAt: "2024-03-14T15:40:45.330Z", locale: "en", // default locale // … } ``` ## Get the published version with `findFirst()` {#find-first} #### GET strapi.documents().findFirst() — findFirst() with status: Return the published version of the first matching document. **JavaScript:** ``` const document = await strapi.documents("api::restaurant.restaurant").findFirst({ status: 'published', }); ``` **Response 200 OK:** ```json { documentId: "a1b2c3d4e5f6g7h8i9j0klm", name: "Biscotte Restaurant", publishedAt: "2024-03-14T15:40:45.330Z", locale: "en", // default locale // … } ``` ## Get the published version with `findMany()` {#find-many} #### GET strapi.documents().findMany() — findMany() with status: Return the published versions of all matching documents. **JavaScript:** ``` const documents = await strapi.documents("api::restaurant.restaurant").findMany({ status: 'published' }); ``` **Response 200 OK:** ```json [ { documentId: "a1b2c3d4e5f6g7h8i9j0klm", name: "Biscotte Restaurant", publishedAt: "2024-03-14T15:40:45.330Z", locale: "en", // default locale // … } // … ] ``` ## `count()` only draft or published versions {#count} To take into account only draft or published versions of documents while [counting documents](/cms/api/document-service#count) with the Document Service API, pass the corresponding `status` parameter: ```js // Count draft documents (also actually includes published documents) const draftsCount = await strapi.documents("api::restaurant.restaurant").count({ status: 'draft' }); ``` ```js // Count only published documents const publishedCount = await strapi.documents("api::restaurant.restaurant").count({ status: 'published' }); ``` :::note Since published documents necessarily also have a draft counterpart, a published document is still counted as having a draft version. This means that counting with the `status: 'draft'` parameter still returns the total number of documents matching other parameters, even if some documents have already been published and are not displayed as "draft" or "modified" in the Content Manager anymore. To count only never-published drafts, pass a [`publicationFilter`](/cms/api/document-service/publication-filter) value such as `'never-published'` or `'never-published-document'`. ::: ## Create a draft and publish it {#create} #### GET strapi.documents().create() — create() with status: Create a new document and immediately publish it. **JavaScript:** ``` await strapi.documents('api::restaurant.restaurant').create({ data: { name: "New Restaurant", }, status: 'published', }) ``` **Response 200 OK:** ```json { documentId: "d41r46wac4xix5vpba7561at", name: "New Restaurant", publishedAt: "2024-03-14T17:29:03.399Z", locale: "en" // default locale // … } ``` ## Update a draft and publish it {#update} #### GET strapi.documents().update() — update() with status: Update an existing document and immediately publish it. **JavaScript:** ``` await strapi.documents('api::restaurant.restaurant').update({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', data: { name: "Biscotte Restaurant (closed)", }, status: 'published', }) ``` **Response 200 OK:** ```json { documentId: "a1b2c3d4e5f6g7h8i9j0klm", name: "Biscotte Restaurant (closed)", publishedAt: "2024-03-14T17:29:03.399Z", locale: "en" // default locale // … } ``` # Entity Service API Source: https://docs.strapi.io/cms/api/entity-service # Entity Service API The Entity Service API is a backend layer that handles complex content structures like components and dynamic zones, providing CRUD operations, filtering, populating relations, and pagination via `strapi.entityService`. :::caution The Entity Service API is deprecated in Strapi v5. Please consider using the [Document Service API](/cms/api/document-service) instead. ::: :::prerequisites Before diving deeper into the Entity Service API documentation, it is recommended that you read the following introductions: - the [backend customization introduction](/cms/backend-customization), - and the [Content APIs introduction](/cms/api/content-api). ::: The Strapi backend provides an Entity Service API, built on top of the [Query Engine API](/cms/api/query-engine/). The Entity Service is the layer that handles Strapi's complex content structures like [components](/cms/backend-customization/models#components-json) and [dynamic zones](/cms/backend-customization/models#dynamic-zones), and uses the Query Engine API under the hood to execute database queries. :::strapi Entity Service API vs. Query Engine API Strapi v4 offers several layers to interact with the backend and build your queries: * The Document Service API is the recommended API to interact with your application's database. The Document Service is the layer that handles Strapi's document model and the complex content structures like components and dynamic zones, which the lower-level layers are not aware of. * The Query Engine API interacts with the database layer at a lower level and is used under the hood to execute database queries. It gives unrestricted internal access to the database layer, but should be used only if the Document Service API does not cover your use case. * If you need direct access to `knex` functions, use `strapi.db.connection`. ::: :::info Disambiguation: Services vs. Entity Service While [services](/cms/backend-customization/services) can use the Entity Service API, services and the Entity Service API are not directly related. You can find more information about the core elements of the Strapi back end in the [back-end customization](/cms/backend-customization) documentation. ::: ## Basic usage The Entity Service is available through `strapi.entityService`: ```js const entry = await strapi.entityService.findOne('api::article.article', 1, { populate: { someRelation: true }, }); ``` ## Available operations The Entity Service API allows the following operations on entities: - [CRUD operations](/cms/api/entity-service/crud): Create, read, update, and delete entities with the Entity Service API. - [Filters](/cms/api/entity-service/filter): Get exactly what you need by filtering entities with your Entity Service API queries. - [Populate](/cms/api/entity-service/populate): Get additional data with your Entity Service API queries by populating relations. - [Order & Pagination](/cms/api/entity-service/order-pagination): Sort and paginate the results of your Entity Service API queries. - [Components/Dynamic Zones](/cms/api/entity-service/components-dynamic-zones): Create and update components and dynamic zones with your Entity Service API queries. # Components and Dynamic Zones Source: https://docs.strapi.io/cms/api/entity-service/components-dynamic-zones # Creating components and dynamic zones with the Entity Service API Use the Entity Service API to create and update components and dynamic zones while creating or updating entries. Components are single objects while dynamic zones are lists of components with a `__component` type identifier. :::caution The Entity Service API is deprecated in Strapi v5. Please consider using the [Document Service API](/cms/api/document-service) instead. ::: The [Entity Service](/cms/api/entity-service) is the layer that handles [components](/cms/backend-customization/models#components-json) and [dynamic zones](/cms/backend-customization/models#dynamic-zones) logic. With the Entity Service API, components and dynamic zones can be [created](#creation) and [updated](#update) while creating or updating entries. ## Creation A [component](/cms/backend-customization/models#components-json) can be created while creating an entry with the Entity Service API: ```js strapi.entityService.create('api::article.article', { data: { myComponent: { foo: 'bar', }, }, }); ``` A [dynamic zone](/cms/backend-customization/models#dynamic-zones) (i.e. a list of components) can be created while creating an entry with the Entity Service API: ```js strapi.entityService.create('api::article.article', { data: { myDynamicZone: [ { __component: 'compo.type', foo: 'bar', }, { __component: 'compo.type2', foo: 'bar', }, ], }, }); ``` ## Update A [component](/cms/backend-customization/models#components-json) can be updated while updating an entry with the Entity Service API. If a component `id` is specified, the component is updated, otherwise the old one is deleted and a new one is created: ```js strapi.entityService.update('api::article.article', 1, { data: { myComponent: { id: 1, // will update component with id: 1 (if not specified, would have deleted it and created a new one) foo: 'bar', }, }, }); ``` A [dynamic zone](/cms/backend-customization/models#dynamic-zones) (i.e. a list of components) can be updated while updating an entry with the Entity Service API. If a component `id` is specified, the component is updated, otherwise the old one is deleted and a new one is created: ```js strapi.entityService.update('api::article.article', 1, { data: { myDynamicZone: [ { // will update id: 2, __component: 'compo.type', foo: 'bar', }, { // will add a new & delete old ones __component: 'compo.type2', foo: 'bar2', }, ], }, }); ``` # CRUD operations Source: https://docs.strapi.io/cms/api/entity-service/crud # CRUD operations with the Entity Service API The Entity Service API performs CRUD operations on content through `findOne()`, `findMany()`, `create()`, `update()`, and `delete()` methods, supporting filtering, pagination, relations, and localization. :::caution The Entity Service API is deprecated in Strapi v5. Please consider using the [Document Service API](/cms/api/document-service) instead. ::: The [Entity Service API](/cms/api/entity-service) is built on top of the the [Query Engine API](/cms/api/query-engine) and uses it to perform CRUD operations on entities. The `uid` parameter used in function calls for this API is a `string` built with the following format: `[category]::[content-type]` where `category` is one of: `admin`, `plugin` or `api`. Examples: - A correct `uid` to get users of the Strapi admin panel is `admin::user`. - A possible `uid` for the Upload plugin could be `plugin::upload.file`. - As the `uid`s for user-defined custom content-types follow the `api::[content-type]` syntax, if a content-type `article` exists, it is referenced by `api::article.article`. :::tip Run the [`strapi content-types:list`](/cms/cli#strapi-content-typeslist) command in a terminal to display all possible content-types' `uid`s for a specific Strapi instance. ::: ## findOne() Finds the first entry matching the parameters. Syntax: `findOne(uid: string, id: ID, parameters: Params)` ⇒ `Entry` ### Parameters | Parameter | Description | Type | | ---------- | --------------- | --------------- | | `fields` | Attributes to return | `String[]` | | `populate` | Relations, components and dynamic zones to [populate](/cms/api/entity-service/populate) | [`PopulateParameter`](/cms/api/entity-service/populate) | | `locale` | Locale code (for example `fr-FR`) when the Internationalization plugin is enabled. Targets the localized variant instead of the default locale. | `string` | ### Example ```js const entry = await strapi.entityService.findOne('api::article.article', 1, { fields: ['title', 'description'], populate: { category: true }, }); ``` ## findMany() Finds entries matching the parameters. Syntax: `findMany(uid: string, parameters: Params)` ⇒ `Entry[]` ### Parameters | Parameter | Description | Type | | ----------- | ------ | -------------- | | `fields` | Attributes to return | `String[]` | | `filters` | [Filters](/cms/api/entity-service/filter) to use | [`FiltersParameters`](/cms/api/entity-service/filter) | | `start` | Number of entries to skip (see [pagination](/cms/api/entity-service/order-pagination#pagination)) | `Number` | | `limit` | Number of entries to return (see [pagination](/cms/api/entity-service/order-pagination#pagination)) | `Number` | | `sort` | [Order](/cms/api/entity-service/order-pagination) definition | [`OrderByParameter`](/cms/api/entity-service/order-pagination) | | `populate` | Relations, components and dynamic zones to [populate](/cms/api/entity-service/populate) | [`PopulateParameter`](/cms/api/entity-service/populate) | | `publicationState` | Publication state, can be:
  • `live` to return only published entries
  • `preview` to return both draft entries & published entries (default)
| `PublicationStateParameter` | | `locale` | Locale code when the Internationalization plugin is enabled. Restricts results to that locale (omit for the default locale). | `string` | ### Example ```js const entries = await strapi.entityService.findMany('api::article.article', { fields: ['title', 'description'], filters: { title: 'Hello World' }, sort: { createdAt: 'DESC' }, populate: { category: true }, }); ```
:::tip To retrieve only draft entries, combine the `preview` publication state and the `publishedAt` fields: ```js const entries = await strapi.entityService.findMany('api::article.article', { publicationState: 'preview', filters: { publishedAt: { $null: true, }, }, }); ::: ## create() Creates one entry and returns it Syntax: `create(uid: string, parameters: Params)` ⇒ `Entry` ### Parameters | Parameter | Description | Type | | ---------- | ----------- | ---------- | | `fields` | Attributes to return | `String[]` | | `populate` | Relations, components and dynamic zones to [populate](/cms/api/entity-service/populate) | [`PopulateParameter`](/cms/api/entity-service/populate) | | `locale` | Locale code when the Internationalization plugin is enabled. Creates the entry for that locale. | `string` | | `data` | Input data | `Object` | :::tip In the `data` object, relations can be managed with the `connect`, `disconnect`, and `set` parameters using the syntax described for the REST API (see [managing relations](/cms/api/rest/relations)). ::: ### Example ```js const entry = await strapi.entityService.create('api::article.article', { data: { title: 'My Article', }, }); ``` ## update() Updates one entry and returns it. :::note `update()` only performs a partial update, so existing fields that are not included won't be replaced. ::: Syntax: `update(uid: string, id: ID, parameters: Params)` ⇒ `Entry` :::tip In the `data` object, relations can be managed with the `connect`, `disconnect`, and `set` parameters using the syntax described for the REST API (see [managing relations](/cms/api/rest/relations)). ::: ### Parameters | Parameter | Description | Type | | ---------- | ------------- | ---------- | | `fields` | Attributes to return | `String[]` | | `populate` | Relations, components and dynamic zones to [populate](/cms/api/entity-service/populate) | [`PopulateParameter`](/cms/api/entity-service/populate) | | `locale` | Locale code when the Internationalization plugin is enabled. Updates the matching localized variant. | `string` | | `data` | Input data | `object` | ### Example ```js const entry = await strapi.entityService.update('api::article.article', 1, { data: { title: 'xxx', }, }); ``` ## delete() Deletes one entry and returns it. Syntax: `delete(uid: string, id: ID, parameters: Params)` ⇒ `Entry` ### Parameters | Parameter | Description | Type | | ---------- | --------- | -------- | | `fields` | Attributes to return | `String[]` | | `populate` | Relations, components and dynamic zones to [populate](/cms/api/entity-service/populate) | [`PopulateParameter`](/cms/api/entity-service/populate) | | `locale` | Locale code when the Internationalization plugin is enabled. Deletes the localized variant that matches this locale. | `string` | ### Example ```js const entry = await strapi.entityService.delete('api::article.article', 1); ``` # Filtering with the Entity Service API Source: https://docs.strapi.io/cms/api/entity-service/filter # Filtering with the Entity Service API Filter Entity Service API query results using logical operators (`$and`, `$or`, `$not`) and attribute operators (`$eq`, `$contains`, `$gt`, `$between`, etc.) with the `filters` parameter in `findMany()`. :::caution The Entity Service API is deprecated in Strapi v5. Please consider using the [Document Service API](/cms/api/document-service) instead. ::: The [Entity Service API](/cms/api/entity-service) offers the ability to filter results found with its [findMany()](/cms/api/entity-service/crud#findmany) method. Results are filtered with the `filters` parameter that accepts [logical operators](#logical-operators) and [attribute operators](#attribute-operators). Every operator should be prefixed with `$`. :::strapi Deep filtering with the various APIs For examples of how to deep filter with the various APIs, please refer to [this blog article](https://strapi.io/blog/deep-filtering-alpha-26). ::: ## Logical operators ### `$and` All nested conditions must be `true`. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { $and: [ { title: 'Hello World', }, { createdAt: { $gt: '2021-11-17T14:28:25.843Z' }, }, ], }, }); ``` `$and` will be used implicitly when passing an object with nested conditions: ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: 'Hello World', createdAt: { $gt: '2021-11-17T14:28:25.843Z' }, }, }); ``` ### `$or` One or many nested conditions must be `true`. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { $or: [ { title: 'Hello World', }, { createdAt: { $gt: '2021-11-17T14:28:25.843Z' }, }, ], }, }); ``` ### `$not` Negates the nested conditions. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { $not: { title: 'Hello World', }, }, }); ``` :::note `$not` can be used as: - a logical operator (e.g. in `filters: { $not: { // conditions… }}`) - [an attribute operator](#not-1) (e.g. in `filters: { attribute-name: $not: { … } }`). ::: :::tip `$and`, `$or` and `$not` operators are nestable inside of another `$and`, `$or` or `$not` operator. ::: ## Attribute Operators :::caution Using these operators may give different results depending on the database's implementation, as the comparison is handled by the database and not by Strapi. ::: ### `$not` Negates the nested condition(s). **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: { $not: { $contains: 'Hello World', }, }, }, }); ``` ### `$eq` Attribute equals input value. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: { $eq: 'Hello World', }, }, }); ``` `$eq` can be omitted: ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: 'Hello World', }, }); ``` ### `$eqi` Attribute equals input value (case-insensitive). **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: { $eqi: 'HELLO World', }, }, }); ``` ### `$ne` Attribute does not equal input value. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: { $ne: 'ABCD', }, }, }); ``` ### `$nei` Attribute does not equal input value (case-insensitive). **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: { $nei: 'abcd', }, }, }); ``` ### `$in` Attribute is contained in the input list. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: { $in: ['Hello', 'Hola', 'Bonjour'], }, }, }); ``` `$in` can be omitted when passing an array of values: ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: ['Hello', 'Hola', 'Bonjour'], }, }); ``` ### `$notIn` Attribute is not contained in the input list. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: { $notIn: ['Hello', 'Hola', 'Bonjour'], }, }, }); ``` ### `$lt` Attribute is less than the input value. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { rating: { $lt: 10, }, }, }); ``` ### `$lte` Attribute is less than or equal to the input value. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { rating: { $lte: 10, }, }, }); ``` ### `$gt` Attribute is greater than the input value. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { rating: { $gt: 5, }, }, }); ``` ### `$gte` Attribute is greater than or equal to the input value. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { rating: { $gte: 5, }, }, }); ``` ### `$between` Attribute is between the 2 input values, boundaries included (e.g., `$between[1, 3]` will also return `1` and `3`). **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { rating: { $between: [1, 20], }, }, }); ``` ### `$contains` Attribute contains the input value (case-sensitive). **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: { $contains: 'Hello', }, }, }); ``` ### `$notContains` Attribute does not contain the input value (case-sensitive). **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: { $notContains: 'Hello', }, }, }); ``` ### `$containsi` Attribute contains the input value. `$containsi` is not case-sensitive, while [$contains](#contains) is. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: { $containsi: 'hello', }, }, }); ``` ### `$notContainsi` Attribute does not contain the input value. `$notContainsi` is not case-sensitive, while [$notContains](#notcontains) is. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: { $notContainsi: 'hello', }, }, }); ``` ### `$startsWith` Attribute starts with input value. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: { $startsWith: 'ABCD', }, }, }); ``` ### `$endsWith` Attribute ends with input value. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: { $endsWith: 'ABCD', }, }, }); ``` ### `$null` Attribute is `null`. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: { $null: true, }, }, }); ``` ### `$notNull` Attribute is not `null`. **Example** ```js const entries = await strapi.entityService.findMany('api::article.article', { filters: { title: { $notNull: true, }, }, }); ``` # Ordering & Pagination with the Entity Service API Source: https://docs.strapi.io/cms/api/entity-service/order-pagination # Ordering and Paginating with the Entity Service API Order and paginate Entity Service API query results using `sort`, `start`/`limit`, or `page`/`pageSize` parameters to control result ordering and retrieve specific data subsets. :::caution The Entity Service API is deprecated in Strapi v5. Please consider using the [Document Service API](/cms/api/document-service) instead. ::: The [Entity Service API](/cms/api/entity-service) offers the ability to [order](#ordering) and [paginate](#pagination) results found with its [findMany()](/cms/api/entity-service/crud#findmany) method. ## Ordering To order results returned by the Entity Service API, use the `sort` parameter. Results can be ordered based on a [single](#single) or on [multiple](#multiple) attribute(s) and can also use [relational ordering](#relational-ordering). ### Single To order results by a single field, pass it to the `sort` parameter either: - as a `string` to sort with the default ascending order, or - as an `object` to define both the field name and the order (i.e. `'asc'` for ascending order or `'desc'` for descending order) ```js strapi.entityService.findMany('api::article.article', { sort: 'id', }); // single with direction strapi.entityService.findMany('api::article.article', { sort: { id: 'desc' }, }); ``` ### Multiple To order results by multiple fields, pass the fields as an array to the `sort` parameter either: - as an array of strings to sort multiple fields using the default ascending order, or - as an array of objects to define both the field name and the order (i.e. `'asc'` for ascending order or `'desc'` for descending order) ```js strapi.entityService.findMany('api::article.article', { sort: ['publishDate', 'name'], }); // multiple with direction strapi.entityService.findMany('api::article.article', { sort: [{ title: 'asc' }, { publishedAt: 'desc' }], }); ``` ### Relational ordering Fields can also be sorted based on fields from relations: ```js strapi.entityService.findMany('api::article.article', { sort: { author: { name: 'asc', }, }, }); ``` ## Pagination To paginate results returned by the Entity Service API, you can use the `start` and `limit` parameters: ```js strapi.entityService.findMany('api::article.article', { start: 10, limit: 15, }); ``` You may instead use the `page` and `pageSize` parameters: ```js strapi.entityService.findMany('api::article.article', { page: 1, pageSize: 15, }); ``` # Populating with the Entity Service API Source: https://docs.strapi.io/cms/api/entity-service/populate # Populating with the Entity Service API The Entity Service API's `populate` parameter retrieves relations, components, and dynamic zones. Use `populate: '*'` for all root-level relations, arrays for specific fields, objects for advanced queries with filters and nested populating, or fragments for polymorphic content structures. :::caution The Entity Service API is deprecated in Strapi v5. Please consider using the [Document Service API](/cms/api/document-service) instead. ::: The [Entity Service API](/cms/api/entity-service) does not populate relations, components or dynamic zones by default, which means an Entity Service API query that does not use the `populate` parameter will not return information about relations, components, or dynamic zones. ## Basic populating To populate all the root level relations, use `populate: '*'`: ```js const entries = await strapi.entityService.findMany('api::article.article', { populate: '*', }); ``` Populate various component or relation fields by passing an array of attribute names: ```js const entries = await strapi.entityService.findMany('api::article.article', { populate: ['componentA', 'relationA'], }); ``` ## Advanced populating An object can be passed for more advanced populating: ```js const entries = await strapi.entityService.findMany('api::article.article', { populate: { relationA: true, repeatableComponent: { fields: ['fieldA'], filters: {}, sort: 'fieldA:asc', populate: { relationB: true, }, }, }, }); ``` Complex populating can be achieved by using the [`filters` parameter](/cms/api/entity-service/filter) and select or populate nested relations or components: ```js const entries = await strapi.entityService.findMany('api::article.article', { populate: { relationA: { filters: { name: { $contains: 'Strapi', }, }, }, repeatableComponent: { fields: ['someAttributeName'], sort: ['someAttributeName'], populate: { componentRelationA: true, }, }, }, }); ``` ## Populate fragments When dealing with polymorphic content structures (dynamic zones, polymorphic relations, etc...), it is possible to use populate fragments to have a better granularity on the populate strategy. ```js const entries = await strapi.entityService.findMany('api::article.article', { populate: { dynamicZone: { on: { 'components.foo': { fields: ['title'], filters: { title: { $contains: 'strapi' } }, }, 'components.bar': { fields: ['name'], }, }, }, morphAuthor: { on: { 'plugin::users-permissions.user': { fields: ['username'], }, 'api::author.author': { fields: ['name'], }, }, }, }, }); ``` # GraphQL API Source: https://docs.strapi.io/cms/api/graphql # GraphQL API The GraphQL API allows querying and mutating content-types with filtering, sorting, and pagination. It uses `documentId` as the unique identifier and provides singular and plural queries with support for relations, media fields, components, dynamic zones, and localization. The GraphQL API allows performing queries and mutations to interact with the [content-types](/cms/backend-customization/models#content-types) through Strapi's [GraphQL plugin](/cms/plugins/graphql). Results can be [filtered](#filters), [sorted](#sorting) and [paginated](#pagination). :::prerequisites To use the GraphQL API, install the [GraphQL](/cms/plugins/graphql) plugin: ```sh yarn add @strapi/plugin-graphql ``` ```sh npm install @strapi/plugin-graphql ``` ::: Once installed, the GraphQL playground is accessible at the `/graphql` URL and can be used to interactively build your queries and mutations and read documentation tailored to your content-types:
The GraphQL plugin exposes only one endpoint that handles all queries and mutations. The default endpoint is `/graphql` and is defined in the [plugins configuration file](/cms/plugins/graphql#code-based-configuration): ```js title="/config/plugins.js|ts" export default { shadowCRUD: true, endpoint: '/graphql', // <— single GraphQL endpoint subscriptions: false, maxLimit: -1, apolloServer: {}, v4CompatibilityMode: process.env.STRAPI_GRAPHQL_V4_COMPATIBILITY_MODE ?? false, }; ``` :::note No GraphQL API to upload media files The GraphQL API does not support media upload. Use the [REST API `POST /upload` endpoint](/cms/api/rest/upload) for all file uploads and use the returned info to link to it in content types. You can still update or delete uploaded files with the `updateUploadFile` and `deleteUploadFile` mutations using media files `id` (see [mutations on media files](#mutations-on-media-files)). ::: :::caution `documentId` only The GraphQL API exposes documents using only the `documentId` field. The previous numeric `id` is no longer available here, although it is still returned by the REST API for backward compatibility (see [breaking change](/cms/migration/v4-to-v5/breaking-changes/use-document-id) for details). ::: ## Queries Queries in GraphQL are used to fetch data without modifying it. When a content-type is added to your project, 2 automatically generated GraphQL queries are added to your schema, named after the content-type's singular and plural API IDs, as in the following example: | Content-type display name | Singular API ID | Plural API ID | |---------------------------|-----------------|---------------| | Restaurant | `restaurant` | `restaurants` |
Singular API ID vs. Plural API ID: Singular API ID and Plural API ID values are defined when creating a content-type in the Content-Type Builder, and can be found while editing a content-type in the admin panel (see [User Guide](/cms/features/content-type-builder#creating-content-types)). You can define custom API IDs while creating the content-type, but these can not modified afterwards.
### Fetch a single document Documents can be fetched by their `documentId`. ```graphql title="Example query: Find a restaurant with its documentId" { restaurant(documentId: "a1b2c3d4e5d6f7g8h9i0jkl") { name description } } ``` ### Fetch multiple documents To fetch multiple documents you can use simple, flat queries or [Relay-style](https://www.apollographql.com/docs/technotes/TN0029-relay-style-connections/) queries: Flat queries return only the requested fields for each document. Relay-style queries end with `_connection` and return a `nodes` array together with a `pageInfo` object. Use Relay-style queries when you need pagination metadata. To fetch multiple documents you can use flat queries like the following: ```graphql title="Example query: Find all restaurants" restaurants { documentId title } ``` Relay-style queries can be used to fetch multiple documents and return meta information: ```graphql title="Example query: Find all restaurants" { restaurants_connection { nodes { documentId name } pageInfo { pageSize page pageCount total } } } ``` #### Fetch relations You can ask to include relation data in your flat queries or in your [Relay-style](https://www.apollographql.com/docs/technotes/TN0029-relay-style-connections/) queries: The following example fetches all documents from the "Restaurant" content-type, and for each of them, also returns some fields for the many-to-many relation with the "Category" content-type: ```graphql title="Example query: Find all restaurants and their associated categories" { restaurants { documentId name description # categories is a many-to-many relation categories { documentId name } } } ``` The following example fetches all documents from the "Restaurant" content-type using a Relay-style query, and for each restaurant, also returns some fields for the many-to-many relation with the "Category" content-type: ```graphql title="Example query: Find all restaurants and their associated categories" { restaurants_connection { nodes { documentId name description # categories is a many-to-many relation categories_connection { nodes { documentId name } } } pageInfo { page pageCount pageSize total } } } ``` :::info For now, `pageInfo` only works for documents at the first level. Future implementations of Strapi might implement `pageInfo` for relations.
Possible use cases for pageInfo: This works: ```graphql { restaurants_connection { nodes { documentId name description # many-to-many relation categories_connection { nodes { documentId name } } } pageInfo { page pageCount pageSize total } } } ``` This does not work: ```graphql {13-19} { restaurants_connection { nodes { documentId name description # many-to-many relation categories_connection { nodes { documentId name } # not supported pageInfo { page pageCount pageSize total } } } pageInfo { page pageCount pageSize total } } }} ```
::: ### Fetch media fields Media fields content is fetched just like other attributes. The following example fetches the `url` attribute value for each `cover` media field attached to each document from the "Restaurants" content-type: ```graphql { restaurants { images { documentId url } } } ``` For multiple media fields, you can use flat queries or [Relay-style](https://www.apollographql.com/docs/technotes/TN0029-relay-style-connections/) queries: The following example fetches some attributes from the `images` multiple media field found in the "Restaurant" content-type: ```graphql { restaurants { images_connection { nodes { documentId url } } } } ``` The following example fetches some attributes from the `images` multiple media field found in the "Restaurant" content-type using a Relay-style query: ```graphql { restaurants { images_connection { nodes { documentId url } } } } ``` :::info For now, `pageInfo` only works for documents. Future implementations of Strapi might implement `pageInfo` for the media fields `_connection` too. ::: ### Fetch components Components content is fetched just like other attributes. The following example fetches the `label`, `start_date`, and `end_date` attributes values for each `closingPeriod` component added to each document from the "Restaurants" content-type: ```graphql { restaurants { closingPeriod { label start_date end_date } } } ``` ### Fetch dynamic zone data Dynamic zones are union types in GraphQL so you need to use [fragments](https://www.apollographql.com/docs/react/data/fragments/) (i.e., with `...on`) to query the fields, passing the component name (with the `ComponentCategoryComponentname` syntax) to [`__typename`](https://www.apollographql.com/docs/apollo-server/schema/schema/#the-__typename-field): The following example fetches data for the `label` attribute of a "Closingperiod" component from the "Default" components category that can be added to the "dz" dynamic zone: ```graphql { restaurants { dz { __typename ...on ComponentDefaultClosingperiod { # define which attributes to return for the component label } } } } ``` ### Fetch draft or published versions {#status} If the [Draft & Publish](/cms/features/draft-and-publish) feature is enabled for the content-type, you can add a `status` parameter to queries to fetch draft or published versions of documents : ```graphql title="Example: Fetch draft versions of documents" query Query($status: PublicationStatus) { restaurants(status: DRAFT) { documentId name publishedAt # should return null } } ``` ```graphql title="Example: Fetch published versions of documents" query Query($status: PublicationStatus) { restaurants(status: PUBLISHED) { documentId name publishedAt } } ``` ### Filter by derived publication cohort {#publication-filter} If the [Draft & Publish](/cms/features/draft-and-publish) feature is enabled, you can add a `publicationFilter` argument to built-in collection and single-type queries. The GraphQL plugin exposes the same cohorts as the REST API and Document Service API through the `PublicationFilter` enum. Combine `publicationFilter` with `status` the same way as for REST (see [Document Service API: `publicationFilter`](/cms/api/document-service/publication-filter#status-combination)). When `status` is omitted, GraphQL defaults to `PUBLISHED` before applying `publicationFilter` (same as REST). Example: `restaurants(publicationFilter: MODIFIED)` returns published rows in the modified cohort; use `status: DRAFT` to return draft rows instead. Built-in root queries (for example `restaurants`, `restaurants_connection`) pass `publicationFilter` down to populated draft & publish relations on nested fields so relation results match the parent query cohort. ```graphql title="Example: Fetch never-published draft documents" query Query($status: PublicationStatus, $publicationFilter: PublicationFilter) { restaurants(status: DRAFT, publicationFilter: NEVER_PUBLISHED) { documentId name publishedAt } } ``` ```graphql title="Example: Fetch published rows without a draft peer" query Query($status: PublicationStatus, $publicationFilter: PublicationFilter) { restaurants(status: PUBLISHED, publicationFilter: PUBLISHED_WITHOUT_DRAFT) { documentId name publishedAt } } ``` ```graphql title="Example: Modified cohort with default PUBLISHED status" query Query { restaurants(publicationFilter: MODIFIED) { documentId name publishedAt } } ``` Available enum values: | GraphQL enum | Document Service / REST value | | ------------ | ----------------------------- | | `NEVER_PUBLISHED` | `never-published` | | `HAS_PUBLISHED_VERSION` | `has-published-version` | | `MODIFIED` | `modified` | | `UNMODIFIED` | `unmodified` | | `NEVER_PUBLISHED_DOCUMENT` | `never-published-document` | | `HAS_PUBLISHED_VERSION_DOCUMENT` | `has-published-version-document` | | `PUBLISHED_WITHOUT_DRAFT` | `published-without-draft` | | `PUBLISHED_WITH_DRAFT` | `published-with-draft` | :::note The deprecated `hasPublishedVersion` boolean argument is still accepted (`true` / `false`) and maps to `NEVER_PUBLISHED_DOCUMENT` / `HAS_PUBLISHED_VERSION_DOCUMENT`. If both `publicationFilter` and `hasPublishedVersion` are provided, `publicationFilter` takes precedence. Prefer `publicationFilter` for new queries. ::: ## Mutations Mutations in GraphQL are used to modify data (e.g. create, update, and delete data). When a content-type is added to your project, 3 automatically generated GraphQL mutations to create, update, and delete documents are added to your schema. For instance, for a "Restaurant" content-type, the following mutations are generated: | Use case | Singular API ID | |---------------------------------------------|---------------------| | Create a new "Restaurant" document | `createRestaurant` | | Update an existing "Restaurant" restaurant | `updateRestaurant` | | Delete an existing "Restaurant" restaurant | `deleteRestaurant` | ### Create a new document When creating new documents, the `data` argument will have an associated input type that is specific to your content-type. For instance, if your Strapi project contains the "Restaurant" content-type, you will have the following: | Mutation | Argument | Input type | |--------------------|------------------|--------------------| | `createRestaurant` | `data` | `RestaurantInput!` | The following example creates a new document for the "Restaurant" content-type and returns its `name` and `documentId`: ```graphql mutation CreateRestaurant($data: RestaurantInput!) { createRestaurant(data: { name: "Pizzeria Arrivederci" }) { name documentId } } ``` When creating a new document, a `documentId` is automatically generated. The implementation of the mutations also supports relational attributes. For example, you can create a new "Category" and attach many "Restaurants" (using their `documentId`) to it by writing your query like follows: ```graphql mutation CreateCategory { createCategory(data: { Name: "Italian Food" restaurants: ["a1b2c3d4e5d6f7g8h9i0jkl", "bf97tfdumkcc8ptahkng4puo"] }) { documentId Name restaurants { documentId name } } } ``` :::tip If the Internationalization (i18n) feature is enabled for your content-type, you can create a document for a specific locale (see [create a new localized document](/cms/api/graphql#locale-create)). ::: ### Update an existing document When updating an existing document , pass the `documentId` and the `data` object containing new content. The `data` argument will have an associated input type that is specific to your content-type. For instance, if your Strapi project contains the "Restaurant" content-type, you will have the following: | Mutation | Argument | Input type | |--------------------|------------------|--------------------| | `updateRestaurant` | `data` | `RestaurantInput!` | For instance, the following example updates an existing document from the "Restaurants" content-type and give it a new name: ```graphql mutation UpdateRestaurant($documentId: ID!, $data: RestaurantInput!) { updateRestaurant( documentId: "bf97tfdumkcc8ptahkng4puo", data: { name: "Pizzeria Amore" } ) { documentId name } } ``` :::tip If the Internationalization (i18n) feature is enabled for your content-type, you can create a document for a specific locale (see [i18n documentation](/cms/api/graphql#locale-update)). ::: #### Update relations You can update relational attributes by passing a `documentId` or an array of `documentId` (depending on the relation type). For instance, the following example updates a document from the "Restaurant" content-type and adds a relation to a document from the "Category" content-type through the `categories` relation field: ```graphql mutation UpdateRestaurant($documentId: ID!, $data: RestaurantInput!) { updateRestaurant( documentId: "slwsiopkelrpxpvpc27953je", data: { categories: ["kbbvj00fjiqoaj85vmylwi17"] } ) { documentId name categories { documentId Name } } } ``` ### Delete a document To delete a document , pass its `documentId`: ```graphql mutation DeleteRestaurant { deleteRestaurant(documentId: "a1b2c3d4e5d6f7g8h9i0jkl") { documentId } } ``` :::tip If the Internationalization (i18n) feature is enabled for your content-type, you can delete a specific localized version of a document (see [i18n documentation](/cms/api/graphql#locale-delete)). ::: ### Mutations on media files :::caution Currently, mutations on media fields use Strapi v4 `id`, not Strapi 5 `documentId`, as unique identifiers for media files. ::: Media fields mutations use files `id`. However, GraphQL API queries in Strapi 5 do not return `id` anymore. Media files `id` can be found: - either in the [Media Library](/cms/features/media-library) from the admin panel, - or by sending REST API `GET` requests that [populate media files](/cms/api/rest/populate-select#population), because REST API requests currently return both `id` and `documentId` for media files. #### Update an uploaded media file When updating an uploaded media file, pass the media's `id` (not its `documentId`) and the `info` object containing new content. The `info` argument will has an associated input type that is specific to media files. For instance, if your Strapi project contains the "Restaurant" content-type, you will have the following: | Mutation | Argument | Input type | |--------------------|------------------|--------------------| | `updateUploadFile` | `info` | `FileInfoInput!` | For instance, the following example updates the `alternativeText` attribute for a media file whose `id` is 3: ```graphql mutation Mutation($updateUploadFileId: ID!, $info: FileInfoInput) { updateUploadFile( id: 3, info: { alternativeText: "New alt text" } ) { documentId url alternativeText } } ``` :::tip If upload mutations return a forbidden access error, ensure proper permissions are set for the Upload plugin (see [User Guide](/cms/features/users-permissions#editing-a-role)). ::: #### Delete an uploaded media file When deleting an uploaded media file, pass the media's `id` (not its `documentId`). ```graphql title="Example: Delete the media file with id 4" mutation DeleteUploadFile($deleteUploadFileId: ID!) { deleteUploadFile(id: 4) { documentId # return its documentId } } ``` :::tip If upload mutations return a forbidden access error, ensure proper permissions are set for the Upload plugin (see [User Guide](/cms/features/users-permissions#editing-a-role)). ::: ## Filters Queries can accept a `filters` parameter with the following syntax: `filters: { field: { operator: value } }` Multiple filters can be combined together, and logical operators (`and`, `or`, `not`) can also be used and accept arrays of objects. When multiple field conditions are combined, they are implicitly joined with `and`. :::tip `and`, `or` and `not` operators can be nested inside one another. ::: The following operators are available: | Operator | Description | | -------------- | ---------------------------------- | | `eq` | Equal | | `eqi` | Equal, case insensitive | | `ne` | Not equal | | `nei` | Not equal, case insensitive | | `lt` | Less than | | `lte` | Less than or equal to | | `gt` | Greater than | | `gte` | Greater than or equal to | | `in` | Included in an array | | `notIn` | Not included in an array | | `contains` | Contains, case sensitive | | `notContains` | Does not contain, case sensitive | | `containsi` | Contains, case insensitive | | `notContainsi` | Does not contain, case insensitive | | `null` | Is null | | `notNull` | Is not null | | `between` | Is between | | `startsWith` | Starts with | | `endsWith` | Ends with | | `and` | Logical `and` | | `or` | Logical `or` | | `not` | Logical `not` | ```graphql title="Simple examples for comparison operators (eq, ne, lt, lte, gt, gte, between)" # eq - returns restaurants with the exact name "Biscotte" { restaurants(filters: { name: { eq: "Biscotte" } }) { name } } # eqi - returns restaurants whose name equals "Biscotte", # comparison is case-insensitive { restaurants(filters: { name: { eqi: "Biscotte" } }) { name } } # ne - returns restaurants whose name is not "Biscotte" { restaurants(filters: { name: { ne: "Biscotte" } }) { name } } # nei - returns restaurants whose name is not "Biscotte", # comparison is case-insensitive { restaurants(filters: { name: { nei: "Biscotte" } }) { name } } # lt - returns restaurants with averagePrice less than 20 { restaurants(filters: { averagePrice: { lt: 20 } }) { name } } # lte - returns restaurants with averagePrice less than or equal to 20 { restaurants(filters: { averagePrice: { lte: 20 } }) { name } } # gt - returns restaurants with averagePrice greater than 20 { restaurants(filters: { averagePrice: { gt: 20 } }) { name } } # gte - returns restaurants with averagePrice greater than or equal to 20 { restaurants(filters: { averagePrice: { gte: 20 } }) { name } } # between - returns restaurants with averagePrice between 10 and 30 { restaurants(filters: { averagePrice: { between: [10, 30] } }) { name } } ``` ```graphql title="Simple examples for membership operators (in, notIn)" # in - returns restaurants with category either "pizza" or "burger" { restaurants(filters: { category: { in: ["pizza", "burger"] } }) { name } } # notIn - returns restaurants whose category is neither "pizza" nor "burger" { restaurants(filters: { category: { notIn: ["pizza", "burger"] } }) { name } } ``` ```graphql title="Simple examples for string matching operators (contains, notContains, containsi, notContains, startsWith, endsWith)" # contains - returns restaurants whose name contains "Pizzeria" { restaurants(filters: { name: { contains: "Pizzeria" } }) { name } } # notContains - returns restaurants whose name does NOT contain "Pizzeria" { restaurants(filters: { name: { notContains: "Pizzeria" } }) { name } } # containsi - returns restaurants whose name contains "pizzeria" (case‑insensitive) { restaurants(filters: { name: { containsi: "pizzeria" } }) { name } } # notContainsi - returns restaurants whose name does NOT contain "pizzeria" (case‑insensitive) { restaurants(filters: { name: { notContainsi: "pizzeria" } }) { name } } # startsWith - returns restaurants whose name starts with "Pizza" { restaurants(filters: { name: { startsWith: "Pizza" } }) { name } } # endsWith - returns restaurants whose name ends with "Inc" { restaurants(filters: { name: { endsWith: "Inc" } }) { name } } ``` ```graphql title="Simple examples for null checks operators (null, notNull)" # null - returns restaurants where description is null { restaurants(filters: { description: { null: true } }) { name } } # notNull - returns restaurants where description is not null { restaurants(filters: { description: { notNull: true } }) { name } } ``` ```graphql title="Simple examples for logical operators (and, or, not)" # and - both category must be "pizza" AND averagePrice must be < 20 { restaurants(filters: { and: [ { category: { eq: "pizza" } }, { averagePrice: { lt: 20 } } ] }) { name } } # or - category is "pizza" OR category is "burger" { restaurants(filters: { or: [ { category: { eq: "pizza" } }, { category: { eq: "burger" } } ] }) { name } } # not - category must NOT be "pizza" { restaurants(filters: { not: { category: { eq: "pizza" } } }) { name } } ``` ```graphql title="Example with nested logical operators: use and, or, and not to find pizzerias under 20 euros" { restaurants( filters: { and: [ { not: { averagePrice: { gte: 20 } } } { or: [ { name: { eq: "Pizzeria" } } { name: { startsWith: "Pizzeria" } } ] } ] } ) { documentId name averagePrice } } ``` :::strapi Deep filtering with the various APIs For examples of how to deep filter with the various APIs, please refer to [this blog article](https://strapi.io/blog/deep-filtering-alpha-26). ::: ## Sorting Queries can accept a `sort` parameter with the following syntax: - to sort based on a single value: `sort: "value"` - to sort based on multiple values: `sort: ["value1", "value2"]` The sorting order can be defined with `:asc` (ascending order, default, can be omitted) or `:desc` (for descending order). ```graphql title="Example: Fetch and sort on name by ascending order" { restaurants(sort: "name") { documentId name } } ``` ```graphql title="Example: Fetch and sort on average price by descending order" { restaurants(sort: "averagePrice:desc") { documentId name averagePrice } } ``` ```graphql title="Example: Fetch and sort on title by ascending order, then on average price by descending order" { restaurants(sort: ["name:asc", "averagePrice:desc"]) { documentId name averagePrice } } ``` ## Pagination [Relay-style](https://www.apollographql.com/docs/technotes/TN0029-relay-style-connections/) queries can accept a `pagination` parameter. Results can be paginated either by page or by offset. :::note Pagination methods can not be mixed. Always use either `page` with `pageSize` or `start` with `limit`. ::: ### Pagination by page | Parameter | Description | Default | | ---------------------- | ----------- | ------- | | `pagination.page` | Page number | 1 | | `pagination.pageSize` | Page size | 10 | ```graphql title="Example query: Pagination by page" { restaurants_connection(pagination: { page: 1, pageSize: 10 }) { nodes { documentId name } pageInfo { page pageSize pageCount total } } } ``` ### Pagination by offset | Parameter | Description | Default | Maximum | | ------------------ | ---------------------------- | ------- | ------- | | `pagination.start` | Start value | 0 | - | | `pagination.limit` | Number of entities to return | 10 | -1 | ```graphql title="Example query: Pagination by offset" { restaurants_connection(pagination: { start: 10, limit: 19 }) { nodes { documentId name } pageInfo { page pageSize pageCount total } } } ``` :::tip The default and maximum values for `pagination.limit` can be [configured in the `./config/plugins.js`](/cms/plugins/graphql#code-based-configuration) file with the `graphql.config.defaultLimit` and `graphql.config.maxLimit` keys. ::: ## `locale` {#locale} The [Internationalization (i18n)](/cms/features/internationalization) feature adds new features to the GraphQL API: - The `locale` field is added to the GraphQL schema. - GraphQL can be used: - to query documents for a specific locale with the `locale` argument - for mutations to [create](#locale-create), [update](#locale-update), and [delete](#locale-delete) documents for a specific locale ### Fetch all documents in a specific locale {#locale-fetch-all} To fetch all documents for a specific locale, pass the `locale` argument to the query: ```graphql query { restaurants(locale: "fr") { documentId name locale } } ``` ```json { "data": { "restaurants": [ { "documentId": "a1b2c3d4e5d6f7g8h9i0jkl", "name": "Restaurant Biscotte", "locale": "fr" }, { "documentId": "m9n8o7p6q5r4s3t2u1v0wxyz", "name": "Pizzeria Arrivederci", "locale": "fr" }, ] } } ``` ### Fetch a document in a specific locale {#locale-fetch} To fetch a documents for a specific locale, pass the `documentId` and the `locale` arguments to the query: **Example query:** ```graphql query Restaurant($documentId: ID!, $locale: I18NLocaleCode) { restaurant(documentId: "a1b2c3d4e5d6f7g8h9i0jkl", locale: "fr") { documentId name description locale } } ``` **Example response:** ```json { "data": { "restaurant": { "documentId": "lviw819d5htwvga8s3kovdij", "name": "Restaurant Biscotte", "description": "Bienvenue au restaurant Biscotte!", "locale": "fr" } } } ``` ### Create a new localized document {#locale-create} The `locale` field can be passed to create a localized document for a specific locale (for more information about mutations with GraphQL, see [the GraphQL API documentation](/cms/api/graphql#create-a-new-document)). ```graphql title="Example: Create a new restaurant for the French locale" mutation CreateRestaurant($data: RestaurantInput!, $locale: I18NLocaleCode) { createRestaurant( data: { name: "Brasserie Bonjour", description: "Description in French goes here" }, locale: "fr" ) { documentId name description locale } ``` ### Update a document for a specific locale {#locale-update} A `locale` argument can be passed in the mutation to update a document for a given locale (for more information about mutations with GraphQL, see [the GraphQL API documentation](/cms/api/graphql#update-an-existing-document)). ```graphql title="Example: Update the description field of restaurant for the French locale" mutation UpdateRestaurant($documentId: ID!, $data: RestaurantInput!, $locale: I18NLocaleCode) { updateRestaurant( documentId: "a1b2c3d4e5d6f7g8h9i0jkl" data: { description: "New description in French" }, locale: "fr" ) { documentId name description locale } ``` ### Delete a locale for a document {#locale-delete} Pass the `locale` argument in the mutation to delete a specific localization for a document : ```graphql mutation DeleteRestaurant($documentId: ID!, $locale: I18NLocaleCode) { deleteRestaurant(documentId: "xzmzdo4k0z73t9i68a7yx2kk", locale: "fr") { documentId } } ``` ## Advanced use cases Click on the following cards for short guides on more advanced use cases leveraging the GraphQL API and Strapi features: - [Advanced queries](/cms/api/graphql/advanced-queries): View examples of multi-level queries and custom resolver chains for the GraphQL API. - [Advanced policies](/cms/api/graphql/advanced-policies): View examples of advanced policies such as conditional visibility and group membership for the GraphQL API. :::info Aggregations not yet available GraphQL aggregations (count, avg, sum, min, max, groupBy) are not yet implemented in `@strapi/plugin-graphql`. This section will be updated when the feature becomes available. In the meantime, you can get a total document count through the REST API (e.g., `GET /api/restaurants?pagination[pageSize]=1` returns `meta.pagination.total`), or write a [custom GraphQL resolver](/cms/api/graphql/advanced-queries#resolver-chains) that uses the [Document Service API](/cms/api/document-service) to compute aggregations. ::: # Advanced policies for GraphQL Source: https://docs.strapi.io/cms/api/graphql/advanced-policies # Advanced policies for the GraphQL API Policies can be attached to GraphQL resolvers to implement complex authorization rules, such as limiting results for unauthenticated users or restricting access based on group membership. Requests sent to the [GraphQL API](/cms/api/graphql) pass through Strapi's [middlewares](/cms/backend-customization/middlewares.md) and [policies](/cms/backend-customization/policies.md) system. Policies can be attached to resolvers to implement complex authorization rules, as shown in the present short guide. For additional information on GraphQL policies, please refer to the [GraphQL plugin configuration](/cms/plugins/graphql#extending-the-schema) documentation. ## Conditional visibility To limit the number of returned entries for unauthenticated users you can write a policy that modifies resolver arguments: ```ts title="/src/policies/limit-public-results.ts" const { state, args } = policyContext; if (!state.user) { args.limit = 4; // only return 4 results for public } return true; }; ``` Register the policy in `/config/policies.ts` and apply it to a resolver: ```ts title="/config/policies.ts" 'api::restaurant.restaurant': { find: [ 'global::limit-public-results' ], }, }; ``` ## Group membership Policies can access `policyContext.state.user` to check group membership, as in the following example: ```ts title="/src/policies/is-group-member.ts" const userGroups = await strapi.query('plugin::users-permissions.group').findMany({ where: { users: { id: state.user.id } }, }); return userGroups.some(g => g.name === config.group); }; ``` Use the policy with the following configuration: ```ts title="/config/policies.ts" 'api::restaurant.restaurant': { find: [{ name: 'global::is-group-member', config: { group: 'editors' } }], }, }; ``` With this setup the resolver only returns results if the authenticated user belongs to the `editors` group. # Advanced queries for GraphQL Source: https://docs.strapi.io/cms/api/graphql/advanced-queries # Advanced queries for the GraphQL API Advanced queries in Strapi's GraphQL API use nested selection sets to fetch multi-level relations and custom resolver chains to reuse logic across resolvers and apply context-specific behavior. Strapi's [GraphQL API](/cms/api/graphql) resolves many queries automatically, but complex data access can require deeper relation fetching or chaining resolvers. The present short guide explains how to handle such advanced scenarios. For additional information, please refer to the [GraphQL customization](/cms/plugins/graphql#extending-the-schema) documentation. ## Multi-level queries Use nested selection sets to fetch relations several levels deep, as in the following example: ```graphql { restaurants { documentId name categories { documentId name parent { documentId name } } } } ``` The GraphQL plugin automatically resolves nested relations. If you need to apply custom logic at a specific level, create a custom resolver for that field. ## Resolver chains Custom resolvers can call other resolvers to reuse existing logic. A common pattern is to resolve permissions or context data in a parent resolver and pass it down to child resolvers, as in the following example: ```js title="/src/api/restaurant/resolvers/restaurant.ts" Query: { restaurants: async (parent, args, ctx) => { const documents = await strapi.documents('api::restaurant.restaurant').findMany(args); return documents.map(doc => ctx.request.graphql.resolve('Restaurant', doc)); }, }, }; ``` In this example the parent resolver fetches restaurants using the [Document Service API](/cms/api/document-service), then delegates to the generated `Restaurant` resolver provided by the plugin so default behavior such as field selection still applies. :::info Aggregations not yet available GraphQL aggregations are not yet implemented in `@strapi/plugin-graphql`. See the [GraphQL advanced use cases](/cms/api/graphql#advanced-use-cases) for details and workarounds. ::: # Using the locale parameter with the GraphQL API Source: https://docs.strapi.io/cms/api/graphql/locale # Use `locale` with the GraphQL API {#graphql} Use the `locale` argument with the GraphQL API to query, create, update, and delete documents for a specific locale. The i18n feature adds new features to the [GraphQL API](/cms/api/graphql): - The `locale` field is added to the GraphQL schema. - GraphQL can be used: - to query documents for a specific locale with the `locale` argument - for mutations to [create](#graphql-create), [update](#graphql-update), and [delete](#graphql-delete) documents for a specific locale ### Fetch all documents in a specific locale {#graphql-fetch-all} To fetch all documents for a specific locale, pass the `locale` argument to the query: ```graphql query { restaurants(locale: "fr") { documentId name locale } } ``` ```json { "data": { "restaurants": [ { "documentId": "a1b2c3d4e5d6f7g8h9i0jkl", "name": "Restaurant Biscotte", "locale": "fr" }, { "documentId": "m9n8o7p6q5r4s3t2u1v0wxyz", "name": "Pizzeria Arrivederci", "locale": "fr" }, ] } } ``` ### Fetch a document in a specific locale {#graphql-fetch} To fetch a documents for a specific locale, pass the `documentId` and the `locale` arguments to the query: **Example query:** ```graphql query Restaurant($documentId: ID!, $locale: I18NLocaleCode) { restaurant(documentId: "a1b2c3d4e5d6f7g8h9i0jkl", locale: "fr") { documentId name description locale } } ``` **Example response:** ```json { "data": { "restaurant": { "documentId": "lviw819d5htwvga8s3kovdij", "name": "Restaurant Biscotte", "description": "Bienvenue au restaurant Biscotte!", "locale": "fr" } } } ``` ### Create a new localized document {#graphql-create} The `locale` field can be passed to create a localized document for a specific locale (for more information about mutations with GraphQL, see [the GraphQL API documentation](/cms/api/graphql#create-a-new-document)). ```graphql title="Example: Create a new restaurant for the French locale" mutation CreateRestaurant($data: RestaurantInput!, $locale: I18NLocaleCode) { createRestaurant( data: { name: "Brasserie Bonjour", description: "Description in French goes here" }, locale: "fr" ) { documentId name description locale } ``` ### Update a document for a specific locale {#graphql-update} A `locale` argument can be passed in the mutation to update a document for a given locale (for more information about mutations with GraphQL, see [the GraphQL API documentation](/cms/api/graphql#update-an-existing-document)). ```graphql title="Example: Update the description field of restaurant for the French locale" mutation UpdateRestaurant($documentId: ID!, $data: RestaurantInput!, $locale: I18NLocaleCode) { updateRestaurant( documentId: "a1b2c3d4e5d6f7g8h9i0jkl" data: { description: "New description in French" }, locale: "fr" ) { documentId name description locale } ``` ### Delete a locale for a document {#graphql-delete} Pass the `locale` argument in the mutation to delete a specific localization for a document : ```graphql mutation DeleteRestaurant($documentId: ID!, $locale: I18NLocaleCode) { deleteRestaurant(documentId: "xzmzdo4k0z73t9i68a7yx2kk", locale: "fr") { documentId } } ``` # OpenAPI specification Source: https://docs.strapi.io/cms/api/openapi # OpenAPI specification generation Strapi provides a CLI tool to automatically generate OpenAPI 3.1.0 specifications documenting all API endpoints, parameters, and responses. The generated specification can be integrated with Swagger UI for interactive API documentation. Strapi provides a command-line tool to generate [OpenAPI](https://www.openapis.org/) specifications for your applications. The CLI tool automatically creates comprehensive API documentation that describes all available endpoints, parameters, and response formats in your Strapi application's Content API. Among the possible use cases, the generated specification can then be easily integrated into documentation tools like [Swagger UI ](https://swagger.io/tools/swagger-ui/). :::callout 🚧 Experimental feature The OpenAPI generation feature is currently experimental. Its behavior and output might change in future releases without following semantic versioning. For additional information and context, please refer to the [Strapi Contributor Docs ](https://contributor.strapi.io/openapi). ::: ## Generating an OpenAPI specification The OpenAPI generation tool is included with Strapi core and doesn't require additional installation. You can use it directly from the command line in any Strapi project to generate comprehensive API documentation. :::note Known limitation for nested component `required` metadata The Admin panel can mark inner fields on a component as required, yet the generated OpenAPI file may still skip a `required` entry for those scalars. The parent object (for instance a component inside a dynamic zone) might list `required` while the nested properties do not. There is more background in [GitHub issue #2236](https://github.com/strapi/documentation/issues/2236). Client generators that trust the raw schema alone can therefore emit types that look looser than what Strapi actually enforces. This area is still experimental, same as the warning banner at the top of the page, so keep validating nested payloads in application code or with the [REST validation helpers](/cms/backend-customization/controllers#sanitization-and-validation-in-controllers) from the controllers guide instead of assuming every Admin rule is mirrored in the exported JSON schema yet. ::: ### CLI usage Executing the command without any arguments will generate a `specification.json` file at the root of your Strapi folder project: ```shell yarn strapi openapi generate ``` ```shell npm run strapi openapi generate ``` You can also path an optional `--output` argument to specify the path and filename, as in the following example: ```bash yarn strapi openapi generate --output ./docs/api-spec.json ``` ```bash npm run strapi openapi generate -- --output ./docs/api-spec.json ``` ### Specification structure and content The generated OpenAPI specification follows the [OpenAPI 3.1.0 standard](https://spec.openapis.org/oas/v3.1.0.html) and could look like in the following shortened example: ```json { "openapi": "3.1.0", "x-powered-by": "strapi", "x-strapi-version": "5.21.0", "info": { "title": "My Strapi API", "description": "API documentation for My Strapi API", "version": "1.0.0" }, "paths": { "/api/articles": { "get": { "operationId": "article/get/articles", "parameters": [ { "name": "fields", "in": "query", "schema": { "type": "array", "items": { "type": "string" } } } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Article" } } } } } } } } } } }, "components": { "schemas": { "Article": { "type": "object", "properties": { "id": { "type": "string" }, "title": { "type": "string" }, "content": { "type": "string" } } } } } } ```
The generated OpenAPI specification includes all available API endpoints in your Strapi application, and information about these endpoints, such as the following: - CRUD operations for all content types - Custom API routes defined in your application - Authentication endpoints for user management - File upload endpoints for media handling - Plugin endpoints from installed plugins ## Integrating with Swagger UI With the following steps you can quickly generate a [Swagger UI](https://swagger.io/)-compatible page: 1. Generate a specification: ```bash yarn strapi openapi generate --output ./public/swagger-spec.json ``` ```bash npm run strapi openapi generate -- --output ./public/swagger-spec.json ``` 2. Update [the `/config/middlewares.js` configuration file](/cms/configurations/middlewares) with the following code: ```js title="/config/middlewares.js" module.exports = [ 'strapi::logger', 'strapi::errors', { name: 'strapi::security', config: { contentSecurityPolicy: { useDefaults: true, directives: { 'script-src': ["'self'", "'unsafe-inline'", 'https://unpkg.com'], 'style-src': ["'self'", "'unsafe-inline'", 'https://unpkg.com'], 'connect-src': ["'self'", 'https:'], 'img-src': ["'self'", 'data:', 'blob:', 'https:'], 'media-src': ["'self'", 'data:', 'blob:'], upgradeInsecureRequests: null, }, }, }, }, 'strapi::cors', 'strapi::poweredBy', 'strapi::query', 'strapi::body', 'strapi::session', 'strapi::favicon', 'strapi::public', ]; ``` ```js title="/config/middlewares.ts" export default [ 'strapi::logger', 'strapi::errors', { name: 'strapi::security', config: { contentSecurityPolicy: { useDefaults: true, directives: { 'script-src': ["'self'", "'unsafe-inline'", 'https://unpkg.com'], 'style-src': ["'self'", "'unsafe-inline'", 'https://unpkg.com'], 'connect-src': ["'self'", 'https:'], 'img-src': ["'self'", 'data:', 'blob:', 'https:'], 'media-src': ["'self'", 'data:', 'blob:'], upgradeInsecureRequests: null, }, }, }, }, 'strapi::cors', 'strapi::poweredBy', 'strapi::query', 'strapi::body', 'strapi::session', 'strapi::favicon', 'strapi::public', ]; ``` This will ensure the Swagger UI display from [unpkg.com](https://unpkg.com/) is not blocked by Strapi's CSP policy handled by the [security middleware](/cms/configurations/middlewares#security). 3. Create a `public/openapi.html` file in your Strapi project to display the Swagger UI, with the following code: ```html API Documentation
``` 4. Restart the Strapi server with `yarn develop` or `npm run develop` and visit the `/openapi.html` page. The Swagger UI should be displayed: ![Swagger UI example with Strapi OpenAPI specification](/img/assets/apis/swagger-open-api.png) # Query Engine API Source: https://docs.strapi.io/cms/api/query-engine # Query Engine API The Query Engine API provides low-level, unrestricted backend access to Strapi's database layer through `strapi.db.query`, supporting single and bulk operations with filtering, populating, ordering, and pagination. The Strapi backend provides a Query Engine API to interact with the database layer at a lower level. :::caution In most cases you should not use the Query Engine API and rather use the [Document Service API](/cms/api/document-service). Only use the Query Engine API if you exactly know what you are doing, for instance if you want to use a lower-level API that directly interacts with unique rows of the database. Please keep in mind that the Query Engine API is not aware of the most advanced Strapi 5 features like Draft & Publish, Internationalization, Content History, and possibly more. This also means that the Query Engine API will not be able to use `documentId` and will use `id`, which means it could lead to unattended consequences at the database level or partial or incomplete compatibility with Strapi 5 features. ::: :::prerequisites Before diving deeper into the Query Engine API documentation, it is recommended that you read the following introductions: - the [backend customization introduction](/cms/backend-customization), - and the [Content APIs introduction](/cms/api/content-api). ::: ## Basic usage The Query Engine is available through `strapi.db.query`: ```js strapi.db.query('api::blog.article').findMany({ // uid syntax: 'api::api-name.content-type-name' where: { title: { $startsWith: '2021', $endsWith: 'v4', }, }, populate: { category: true, }, }); ``` ## Available operations The Query Engine allows the following operations on database entries: - [Single operations](/cms/api/query-engine/single-operations): Create, read, update, and delete single database entries with the Query Engine API. - [Bulk operations](/cms/api/query-engine/bulk-operations): Create, read, update, and delete multiple database entries with the Query Engine API. - [Filters](/cms/api/query-engine/filtering): Get exactly what you need by filtering database entries with the Query Engine API. - [Populate](/cms/api/query-engine/populating): Get additional data with your Query Engine API queries by populating relations. - [Order & Pagination](/cms/api/query-engine/order-pagination): Sort and paginate the results of your Query Engine API queries. # Bulk Operations Source: https://docs.strapi.io/cms/api/query-engine/bulk-operations # Bulk Operations with the Query Engine API Bulk Operations with the Query Engine API enable you to create, update, delete, and count multiple entries at once using `createMany()`, `updateMany()`, `deleteMany()`, and `count()` methods. :::caution In most cases you should not use the Query Engine API and rather use the [Document Service API](/cms/api/document-service). Only use the Query Engine API if you exactly know what you are doing, for instance if you want to use a lower-level API that directly interacts with unique rows of the database. Please keep in mind that the Query Engine API is not aware of the most advanced Strapi 5 features like Draft & Publish, Internationalization, Content History, and possibly more. This also means that the Query Engine API will not be able to use `documentId` and will use `id`, which means it could lead to unattended consequences at the database level or partial or incomplete compatibility with Strapi 5 features. ::: :::prerequisites Before diving deeper into the Query Engine API documentation, it is recommended that you read the following introductions: - the [backend customization introduction](/cms/backend-customization), - and the [Content APIs introduction](/cms/api/content-api). ::: :::caution To avoid performance issues, bulk operations are not allowed on relations. ::: ## createMany() Creates multiple entries. Syntax: `createMany(parameters) => { count: number, ids: id[] }` ### Parameters | Parameter | Type | Description | | --------- | ---------------- | ------------------- | | `data` | Array of objects | Array of input data | :::caution * MySQL will only return an array of one id containing the last inserted id, not the entire list. * Prior to Strapi v4.9.0, `createMany()` only returns the `count`. ::: ### Example ```js await strapi.db.query("api::blog.article").createMany({ data: [ { title: "ABCD", }, { title: "EFGH", }, ], }); // { count: 2 , ids: [1,2]} ``` ## updateMany() Updates multiple entries matching the parameters. Syntax: `updateMany(parameters) => { count: number }` ### Parameters | Parameter | Type | Description | | --------- | --------------------------------------------------------- | ------------------------------------------------------- | | `where` | [`WhereParameter`](/cms/api/query-engine/filtering/) | [Filters](/cms/api/query-engine/filtering/) to use | | `data` | Object | Input data | ### Example ```js await strapi.db.query("api::shop.article").updateMany({ where: { price: 20, }, data: { price: 18, }, }); // { count: 42 } ``` ## deleteMany() Deletes multiple entries matching the parameters. Syntax: `deleteMany(parameters) => { count: number }` ### Parameters | Parameter | Type | Description | | --------- | --------------------------------------------------------- | ------------------------------------------------------- | | `where` | [`WhereParameter`](/cms/api/query-engine/filtering/) | [Filters](/cms/api/query-engine/filtering/) to use | ### Example ```js await strapi.db.query("api::blog.article").deleteMany({ where: { title: { $startsWith: "v3", }, }, }); // { count: 42 } ``` ## Aggregations ### count() Counts entries matching the parameters. Syntax: `count(parameters) => number` #### Parameters | Parameter | Type | Description | | --------- | --------------------------------------------------------- | ------------------------------------------------------- | | `where` | [`WhereParameter`](/cms/api/query-engine/filtering/) | [Filters](/cms/api/query-engine/filtering/) to use | ```js const count = await strapi.db.query("api::blog.article").count({ where: { title: { $startsWith: "v3", }, }, }); // 12 ``` # Filtering with the Query Engine API Source: https://docs.strapi.io/cms/api/query-engine/filtering # Filtering with the Query Engine API The Query Engine API filters query results using the `where` parameter with logical operators (`$and`, `$or`, `$not`) and attribute operators (comparison, string matching, range) prefixed with `$`. :::caution In most cases you should not use the Query Engine API and rather use the [Document Service API](/cms/api/document-service). Only use the Query Engine API if you exactly know what you are doing, for instance if you want to use a lower-level API that directly interacts with unique rows of the database. Please keep in mind that the Query Engine API is not aware of the most advanced Strapi 5 features like Draft & Publish, Internationalization, Content History, and possibly more. This also means that the Query Engine API will not be able to use `documentId` and will use `id`, which means it could lead to unattended consequences at the database level or partial or incomplete compatibility with Strapi 5 features. ::: :::prerequisites Before diving deeper into the Query Engine API documentation, it is recommended that you read the following introductions: - the [backend customization introduction](/cms/backend-customization), - and the [Content APIs introduction](/cms/api/content-api). ::: The [Query Engine API](/cms/api/query-engine/) offers the ability to filter results found with its [findMany()](/cms/api/query-engine/single-operations#findmany) method. Results are filtered with the `where` parameter that accepts [logical operators](#logical-operators) and [attribute operators](#attribute-operators). Every operator should be prefixed with `$`. :::strapi Deep filtering with the various APIs For examples of how to deep filter with the various APIs, please refer to [this blog article](https://strapi.io/blog/deep-filtering-alpha-26). ::: ## Logical operators ### `$and` All nested conditions must be `true`. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { $and: [ { title: 'Hello World', }, { createdAt: { $gt: '2021-11-17T14:28:25.843Z' }, }, ], }, }); ``` `$and` is used implicitly when passing an object with nested conditions: ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: 'Hello World', createdAt: { $gt: '2021-11-17T14:28:25.843Z' }, }, }); ``` ### `$or` One or many nested conditions must be `true`. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { $or: [ { title: 'Hello World', }, { createdAt: { $gt: '2021-11-17T14:28:25.843Z' }, }, ], }, }); ``` ### `$not` Negates the nested conditions. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { $not: { title: 'Hello World', }, }, }); ``` :::note `$not` can be used: - as a logical operator (e.g. in `where: { $not: { // conditions… }}`) - or [as an attribute operator](#not-1) (e.g. in `where: { attribute-name: $not: { … } }`). ::: :::tip `$and`, `$or` and `$not` operators are nestable inside of another `$and`, `$or` or `$not` operator. ::: ## Attribute Operators :::caution Using these operators may give different results depending on the database's implementation, as the comparison is handled by the database and not by Strapi. ::: ### `$not` Negates nested condition(s). **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: { $not: { $contains: 'Hello World', }, }, }, }); ``` ### `$eq` Attribute equals input value. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: { $eq: 'Hello World', }, }, }); ``` `$eq` can be omitted: ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: 'Hello World', }, }); ``` ### `$eqi` Attribute equals input value (case-insensitive). **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: { $eqi: 'HELLO World', }, }, }); ``` ### `$ne` Attribute does not equal input value. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: { $ne: 'ABCD', }, }, }); ``` ### `$nei` Attribute does not equal input value (case-insensitive). **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: { $nei: 'abcd', }, }, }); ``` ### `$in` Attribute is contained in the input list. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: { $in: ['Hello', 'Hola', 'Bonjour'], }, }, }); ``` `$in` can be omitted when passing an array of values: ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: ['Hello', 'Hola', 'Bonjour'], }, }); ``` ### `$notIn` Attribute is not contained in the input list. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: { $notIn: ['Hello', 'Hola', 'Bonjour'], }, }, }); ``` ### `$lt` Attribute is less than the input value. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { rating: { $lt: 10, }, }, }); ``` ### `$lte` Attribute is less than or equal to the input value. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { rating: { $lte: 10, }, }, }); ``` ### `$gt` Attribute is greater than the input value. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { rating: { $gt: 5, }, }, }); ``` ### `$gte` Attribute is greater than or equal to the input value. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { rating: { $gte: 5, }, }, }); ``` ### `$between` Attribute is between the 2 input values, boundaries included (e.g., `$between[1, 3]` will also return `1` and `3`). **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { rating: { $between: [1, 20], }, }, }); ``` ### `$contains` Attribute contains the input value (case-sensitive). **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: { $contains: 'Hello', }, }, }); ``` ### `$notContains` Attribute does not contain the input value (case-sensitive). **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: { $notContains: 'Hello', }, }, }); ``` ### `$containsi` Attribute contains the input value. `$containsi` is not case-sensitive, while [$contains](#contains) is. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: { $containsi: 'hello', }, }, }); ``` ### `$notContainsi` Attribute does not contain the input value. `$notContainsi` is not case-sensitive, while [$notContains](#notcontains) is. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: { $notContainsi: 'hello', }, }, }); ``` ### `$startsWith` Attribute starts with input value. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: { $startsWith: 'ABCD', }, }, }); ``` ### `$endsWith` Attribute ends with input value. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: { $endsWith: 'ABCD', }, }, }); ``` ### `$null` Attribute is `null`. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: { $null: true, }, }, }); ``` ### `$notNull` Attribute is not `null`. **Example** ```js const entries = await strapi.db.query('api::article.article').findMany({ where: { title: { $notNull: true, }, }, }); ``` # Ordering & Pagination with the Query Engine API Source: https://docs.strapi.io/cms/api/query-engine/order-pagination # Ordering and Paginating with the Query Engine API The Query Engine API supports ordering results with the `orderBy` parameter on single or multiple attributes, including relational ordering, and pagination with `offset` and `limit` parameters. :::caution In most cases you should not use the Query Engine API and rather use the [Document Service API](/cms/api/document-service). Only use the Query Engine API if you exactly know what you are doing, for instance if you want to use a lower-level API that directly interacts with unique rows of the database. Please keep in mind that the Query Engine API is not aware of the most advanced Strapi 5 features like Draft & Publish, Internationalization, Content History, and possibly more. This also means that the Query Engine API will not be able to use `documentId` and will use `id`, which means it could lead to unattended consequences at the database level or partial or incomplete compatibility with Strapi 5 features. ::: :::prerequisites Before diving deeper into the Query Engine API documentation, it is recommended that you read the following introductions: - the [backend customization introduction](/cms/backend-customization), - and the [Content APIs introduction](/cms/api/content-api). ::: The [Query Engine API](/cms/api/query-engine) offers the ability to [order](#ordering) and [paginate](#pagination) results. ## Ordering To order results returned by the Query Engine, use the `orderBy` parameter. Results can be ordered based on a [single](#single) or on [multiple](#multiple) attributes and can also use [relational ordering](#relational-ordering). ### Single ```js strapi.db.query('api::article.article').findMany({ orderBy: 'id', }); // single with direction strapi.db.query('api::article.article').findMany({ orderBy: { id: 'asc' }, }); ``` ### Multiple ```js strapi.db.query('api::article.article').findMany({ orderBy: ['id', 'name'], }); // multiple with direction strapi.db.query('api::article.article').findMany({ orderBy: [{ title: 'asc' }, { publishedAt: 'desc' }], }); ``` ### Relational ordering ```js strapi.db.query('api::article.article').findMany({ orderBy: { author: { name: 'asc', }, }, }); ``` ## Pagination To paginate results returned by the Query Engine API, use the `offset` and `limit` parameters: ```js strapi.db.query('api::article.article').findMany({ offset: 15, limit: 10, }); ``` # Populating with the Query Engine API Source: https://docs.strapi.io/cms/api/query-engine/populating # Populating with the Query Engine API The Query Engine API's `populate` parameter loads related data in queries, supporting basic population, selective attributes, filtering nested relations, and polymorphic structures via populate fragments. :::caution In most cases you should not use the Query Engine API and rather use the [Document Service API](/cms/api/document-service). Only use the Query Engine API if you exactly know what you are doing, for instance if you want to use a lower-level API that directly interacts with unique rows of the database. Please keep in mind that the Query Engine API is not aware of the most advanced Strapi 5 features like Draft & Publish, Internationalization, Content History, and possibly more. This also means that the Query Engine API will not be able to use `documentId` and will use `id`, which means it could lead to unattended consequences at the database level or partial or incomplete compatibility with Strapi 5 features. ::: :::prerequisites Before diving deeper into the Query Engine API documentation, it is recommended that you read the following introductions: - the [backend customization introduction](/cms/backend-customization), - and the [Content APIs introduction](/cms/api/content-api). ::: Relations and components have a unified API for populating them. To populate all the root level relations, use `populate: true`: ```js strapi.db.query('api::article.article').findMany({ populate: true, }); ``` Select which data to populate by passing an array of attribute names: ```js strapi.db.query('api::article.article').findMany({ populate: ['componentA', 'relationA'], }); ``` An object can be passed for more advanced usage: ```js strapi.db.query('api::article.article').findMany({ populate: { componentB: true, dynamiczoneA: true, relation: someLogic || true, }, }); ``` Complex populating can also be achieved by applying `where` filters and select or populate nested relations: ```js strapi.db.query('api::article.article').findMany({ populate: { relationA: { where: { name: { $contains: 'Strapi', }, }, }, repeatableComponent: { select: ['someAttributeName'], orderBy: ['someAttributeName'], populate: { componentRelationA: true, }, }, dynamiczoneA: true, }, }); ``` When dealing with polymorphic content structures (dynamic zones, polymorphic relations, etc...), it is possible to use populate fragments to have a better granularity on the populate strategy. ```js strapi.db.query('api::article.article').findMany('api::article.article', { populate: { dynamicZone: { on: { 'components.foo': { select: ['title'], where: { title: { $contains: 'strapi' } }, }, 'components.bar': { select: ['name'], }, }, }, morphAuthor: { on: { 'plugin::users-permissions.user': { select: ['username'], }, 'api::author.author': { select: ['name'], }, }, }, }, }); ``` # Single Operations Source: https://docs.strapi.io/cms/api/query-engine/single-operations # Single Operations with the Query Engine API The Query Engine API provides methods to find, create, update, and delete individual entries with filtering, selection, pagination, and relation population options. :::caution In most cases you should not use the Query Engine API and rather use the [Document Service API](/cms/api/document-service). Only use the Query Engine API if you exactly know what you are doing, for instance if you want to use a lower-level API that directly interacts with unique rows of the database. Please keep in mind that the Query Engine API is not aware of the most advanced Strapi 5 features like Draft & Publish, Internationalization, Content History, and possibly more. This also means that the Query Engine API will not be able to use `documentId` and will use `id`, which means it could lead to unattended consequences at the database level or partial or incomplete compatibility with Strapi 5 features. ::: :::prerequisites Before diving deeper into the Query Engine API documentation, it is recommended that you read the following introductions: - the [backend customization introduction](/cms/backend-customization), - and the [Content APIs introduction](/cms/api/content-api). ::: ## findOne() :::note Only use the Query Engine's `findOne()` method if the [Document Service's `findOne()`](/cms/api/document-service#findone) method can't cover your use case. ::: Finds the first entry matching the parameters. Syntax: `findOne(parameters) ⇒ Entry` ### Parameters | Parameter | Type | Description | | ---------- | -------------- | --------- | | `select` | String, or Array of strings | [Attributes](/cms/backend-customization/models#model-attributes) to return | | `where` | [`WhereParameter`](/cms/api/query-engine/filtering/) | [Filters](/cms/api/query-engine/filtering/) to use | | `offset` | Integer | Number of entries to skip | | `orderBy` | [`OrderByParameter`](/cms/api/query-engine/order-pagination/) | [Order](/cms/api/query-engine/order-pagination/) definition | | `populate` | [`PopulateParameter`](/cms/api/query-engine/populating/) | Relations to [populate](/cms/api/query-engine/populating/) | ### Example ```js const entry = await strapi.db.query('api::blog.article').findOne({ select: ['title', 'description'], where: { title: 'Hello World' }, populate: { category: true }, }); ``` ## findMany() :::note Only use the Query Engine's `findMany()` method if the [Document Service `findMany()`](/cms/api/document-service#findmany) method can't cover your use case. ::: Finds entries matching the parameters. Syntax: `findMany(parameters) ⇒ Entry[]` ### Parameters | Parameter | Type | Description | | --------- | ------------------------------ | ------------------------------------------ | | `select` | String, or Array of strings | [Attributes](/cms/backend-customization/models#model-attributes) to return | | `where` | [`WhereParameter`](/cms/api/query-engine/filtering/) | [Filters](/cms/api/query-engine/filtering/) to use | | `limit` | Integer | Number of entries to return | | `offset` | Integer | Number of entries to skip | | `orderBy` | [`OrderByParameter`](/cms/api/query-engine/order-pagination/) | [Order](/cms/api/query-engine/order-pagination/) definition | | `populate` | [`PopulateParameter`](/cms/api/query-engine/populating/) | Relations to [populate](/cms/api/query-engine/populating/) | ### Example ```js const entries = await strapi.db.query('api::blog.article').findMany({ select: ['title', 'description'], where: { title: 'Hello World' }, orderBy: { publishedAt: 'DESC' }, populate: { category: true }, }); ``` ## findWithCount() Finds and counts entries matching the parameters. Syntax: `findWithCount(parameters) => [Entry[], number]` ### Parameters | Parameter | Type | Description | | --------- | ------------------------------ | ------------------------------------------ | | `select` | String, or Array of strings | [Attributes](/cms/backend-customization/models#model-attributes) to return | | `where` | [`WhereParameter`](/cms/api/query-engine/filtering/) | [Filters](/cms/api/query-engine/filtering/) to use | | `limit` | Integer | Number of entries to return | | `offset` | Integer | Number of entries to skip | | `orderBy` | [`OrderByParameter`](/cms/api/query-engine/order-pagination/) | [Order](/cms/api/query-engine/order-pagination/) definition | | `populate` | [`PopulateParameter`](/cms/api/query-engine/populating/) | Relations to [populate](/cms/api/query-engine/populating/) | ### Example ```js const [entries, count] = await strapi.db.query('api::blog.article').findWithCount({ select: ['title', 'description'], where: { title: 'Hello World' }, orderBy: { title: 'DESC' }, populate: { category: true }, }); ``` ## create() :::note Only use the Query Engine's `create()` method if the [Document Service `create()` method](/cms/api/document-service#create) can't cover your use case. ::: Creates one entry and returns it. Syntax: `create(parameters) => Entry` ### Parameters | Parameter | Type | Description | | --------- | ------------------------------ | ------------------------------------------ | | `select` | String, or Array of strings | [Attributes](/cms/backend-customization/models#model-attributes) to return | | `populate` | [`PopulateParameter`](/cms/api/query-engine/populating/) | Relations to [populate](/cms/api/query-engine/populating/) | | `data` | Object | Input data | ### Example ```js const entry = await strapi.db.query('api::blog.article').create({ data: { title: 'My Article', }, }); ``` ## update() :::note Only use the Query Engine's `update()` method if the [Document Service `update()`](/cms/api/document-service#update) method can't cover your use case. ::: Updates one entry and returns it. Syntax: `update(parameters) => Entry` ### Parameters | Parameter | Type | Description | | --------- | ------------------------------ | ------------------------------------------ | | `select` | String, or Array of strings | [Attributes](/cms/backend-customization/models#model-attributes) to return | | `populate` | [`PopulateParameter`](/cms/api/query-engine/populating/) | Relations to [populate](/cms/api/query-engine/populating/) | `where` | [`WhereParameter`](/cms/api/query-engine/filtering/) | [Filters](/cms/api/query-engine/filtering/) to use | | `data` | Object | Input data | ### Example ```js const entry = await strapi.db.query('api::blog.article').update({ where: { id: 1 }, data: { title: 'xxx', }, }); ``` ## delete() :::note Only use the Query Engine's `delete()` method if the [Document Service `delete()`](/cms/api/document-service#delete) method can't cover your use case. ::: Deletes one entry and returns it. Syntax: `delete(parameters) => Entry` ### Parameters | Parameter | Type | Description | | --------- | ------------------------------ | ------------------------------------------ | | `select` | String, or Array of strings | [Attributes](/cms/backend-customization/models#model-attributes) to return | | `populate` | [`PopulateParameter`](/cms/api/query-engine/populating/) | Relations to [populate](/cms/api/query-engine/populating/) | `where` | [`WhereParameter`](/cms/api/query-engine/filtering/) | [Filters](/cms/api/query-engine/filtering/) to use | ### Example ```js const entry = await strapi.db.query('api::blog.article').delete({ where: { id: 1 }, }); ``` # REST API reference Source: https://docs.strapi.io/cms/api/rest # REST API reference Strapi's REST API automatically generates endpoints for content-types to fetch, create, update, and delete documents using GET, POST, PUT, and DELETE methods, with support for filtering, sorting, field selection, and relation population. The REST API allows accessing the [content-types](/cms/backend-customization/models) through API endpoints. Strapi automatically creates [API endpoints](#endpoints) when a content-type is created. [API parameters](/cms/api/rest/parameters) can be used when querying API endpoints to refine the results. This section of the documentation is for the REST API reference for content-types. We also have [guides](/cms/api/rest/guides/intro) available for specific use cases. :::prerequisites All content types are private by default and need to be either made public or queries need to be authenticated with the proper permissions. See the [Quick Start Guide](/cms/quick-start#step-8-set-roles--permissions), the user guide for the [Users & Permissions feature](/cms/features/users-permissions#roles), and [API tokens configuration documentation](/cms/features/api-tokens) for more details. ::: :::note By default, the REST API responses only include top-level fields and does not populate any relations, media fields, components, or dynamic zones. Use the [`populate` parameter](/cms/api/rest/populate-select) to populate specific fields. Ensure that the find permission is given to the field(s) for the relation(s) you populate. ::: :::tip Performance best practices For production applications, be intentional about data fetching: use explicit population, limit population depth, and centralize population logic in route middlewares. See [Building High-Performance Strapi Applications](https://strapi.io/blog/building-high-performance-strapi-applications-common-pitfalls-and-best-practices) on the Strapi blog for a comprehensive guide. ::: :::strapi Strapi Client The [Strapi Client](/cms/api/client) library simplifies interactions with your Strapi back end, providing a way to fetch, create, update, and delete content. ::: ## Endpoints For each Content-Type, the following endpoints are automatically generated:
Plural API ID vs. Singular API ID: In the following tables: - `:singularApiId` refers to the value of the "API ID (Singular)" field of the content-type, - and `:pluralApiId` refers to the value of the "API ID (Plural)" field of the content-type. These values are defined when creating a content-type in the Content-Type Builder, and can be found while editing a content-type in the admin panel (see [User Guide](/cms/features/content-type-builder#creating-content-types)). For instance, by default, for an "Article" content-type: - `:singularApiId` will be `article` - `:pluralApiId` will be `articles`
| Method | URL | Description | | -------- | ------------------------------- | ------------------------------------- | | `GET` | `/api/:pluralApiId` | [Get a list of documents](#get-all) | | `POST` | `/api/:pluralApiId` | [Create a document](#create) | | `GET` | `/api/:pluralApiId/:documentId` | [Get a document](#get) | | `PUT` | `/api/:pluralApiId/:documentId` | [Update a document](#update) | | `DELETE` | `/api/:pluralApiId/:documentId` | [Delete a document](#delete) | | Method | URL | Description | | -------- | --------------------- | ------------------------------------------ | | `GET` | `/api/:singularApiId` | [Get a document](#get) | | `PUT` | `/api/:singularApiId` | [Update/Create a document](#update) | | `DELETE` | `/api/:singularApiId` | [Delete a document](#delete) | :::strapi Upload API The Upload package (which powers the [Media Library feature](/cms/features/media-library)) has a specific API accessible through its [`/api/upload` endpoints](/cms/api/rest/upload). ::: :::note [Components](/cms/backend-customization/models#components-json) don't have API endpoints. ::: ## Requests :::strapi Strapi 5 vs. Strapi v4 Strapi 5's Content API includes 2 major differences with Strapi v4: - The response format has been flattened, which means attributes are no longer nested in a `data.attributes` object and are directly accessible at the first level of the `data` object (e.g., a content-type's "title" attribute is accessed with `data.title`). - Strapi 5 now uses **documents** and documents are accessed by their `documentId` (see [breaking change entry](/cms/migration/v4-to-v5/breaking-changes/use-document-id) for details) ::: Requests return a response as an object which usually includes the following keys: - `data`: the response data itself, which could be: - a single document, as an object with the following keys: - `id` (integer) - `documentId` (string), which is the unique identifier to use when querying a given document, - the attributes (each attribute's type depends on the attribute, see [models attributes](/cms/backend-customization/models#model-attributes) documentation for details) - `meta` (object) - a list of documents, as an array of objects - a custom response - `meta` (object): information about pagination, publication state, available locales, etc. - `error` (object, _optional_): information about any [error](/cms/error-handling) thrown by the request :::note Some plugins (including Users & Permissions and Upload) may not follow this response format. ::: ### Get documents {#get-all} :::tip Tip: Strapi 5 vs. Strapi 4 In Strapi 5 the response format has been flattened, and attributes are directly accessible from the `data` object instead of being nested in `data.attributes`. You can pass an optional header while you're migrating to Strapi 5 (see the [related breaking change](/cms/migration/v4-to-v5/breaking-changes/new-response-format)). ::: #### GET /api/:pluralApiId — List documents Returns a paginated list of documents. Supports filtering, sorting, field selection, and relation population. **Parameters:** - `sort` (string | string[]): Sort by field. Use `field:asc` or `field:desc` - `filters` (object): Filter with operators: `$eq`, `$contains`, `$gt`, `$lt`. See filtering (/cms/api/rest/filters). - `populate` (string | object): Relations and components to include. Use `*` for all. See populate (/cms/api/rest/populate-select). - `fields` (string[]): Select specific fields to return. See field selection (/cms/api/rest/populate-select#field-selection). - `pagination[page]` (integer): Page number. Default: `1` - `pagination[pageSize]` (integer): Items per page. Default `25`, max `100` - `locale` (string): Locale of the documents to fetch. See locale (/cms/api/rest/locale). - `status` (string): `published` or `draft`. See status (/cms/api/rest/status). **cURL:** ``` curl 'http://localhost:1337/api/restaurants' \\ -H 'Authorization: Bearer ' ``` **JavaScript:** ``` const response = await fetch( 'http://localhost:1337/api/restaurants', { headers: { Authorization: 'Bearer ', }, } ); const data = await response.json(); ``` **Response 200 OK:** ```json { "data": [ { "id": 2, "documentId": "hgv1vny5cebq2l3czil1rpb3", "Name": "BMK Paris Bamako", "Description": null, "createdAt": "2024-03-06T13:42:05.098Z", "updatedAt": "2024-03-06T13:42:05.098Z", "publishedAt": "2024-03-06T13:42:05.103Z", "locale": "en" }, { "id": 4, "documentId": "znrlzntu9ei5onjvwfaalu2v", "Name": "Biscotte Restaurant", "Description": [ { "type": "paragraph", "children": [ { "type": "text", "text": "Welcome to Biscotte restaurant! Restaurant Biscotte offers a cuisine based on fresh, quality products, often local, organic when possible, and always produced by passionate producers." } ] } ], "createdAt": "2024-03-06T13:43:30.172Z", "updatedAt": "2024-03-06T13:43:30.172Z", "publishedAt": "2024-03-06T13:43:30.175Z", "locale": "en" } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 2 } } } ``` ### Get a document {#get} :::strapi Strapi 5 vs. Strapi v4 In Strapi 5, a specific document is reached by its `documentId`. ::: #### GET /api/:pluralApiId/:documentId — Get a document Returns a single document by its documentId. Supports field selection and relation population. **Parameters:** - `pluralApiId` (string, required): Plural API ID of the content-type (e.g. `restaurants`) - `documentId` (string, required): Unique document identifier **cURL:** ``` curl 'http://localhost:1337/api/restaurants/znrlzntu9ei5onjvwfaalu2v' \\ -H 'Authorization: Bearer ' ``` **JavaScript:** ``` const response = await fetch( 'http://localhost:1337/api/restaurants/znrlzntu9ei5onjvwfaalu2v', { headers: { Authorization: 'Bearer ', }, } ); const data = await response.json(); ``` **Response 200 OK:** ```json { "data": { "id": 6, "documentId": "znrlzntu9ei5onjvwfaalu2v", "Name": "Biscotte Restaurant", "Description": [ { "type": "paragraph", "children": [ { "type": "text", "text": "Welcome to Biscotte restaurant! Restaurant Biscotte offers a cuisine based on fresh, quality products." } ] } ], "createdAt": "2024-02-27T10:19:04.953Z", "updatedAt": "2024-03-05T15:52:05.591Z", "publishedAt": "2024-03-05T15:52:05.600Z", "locale": "en" }, "meta": {} } ``` **Response 200 Not Found:** ```json { "error": { "status": 404, "name": "NotFoundError", "message": "Document not found" } } ``` ### Create a document {#create} If the [Internationalization (i18n) plugin](/cms/features/internationalization) is installed, it's possible to use POST requests to the REST API to [create localized documents](/cms/api/rest/locale#rest-delete). :::note While creating a document, you can define its relations and their order (see [Managing relations through the REST API](/cms/api/rest/relations.md) for more details). ::: :::info Dynamic zones When you POST a document, each object inside a dynamic zone array must include `__component` with that variant's UID (for example `shared.slider`). Put `__component` on the object that represents one row in the dynamic zone. Nested fields inside that object should mirror the JSON you get back when the same document is fetched with `populate` so you do not place `__component` on inner objects unless the schema treats that level as another discriminated structure. ::: #### POST /api/:pluralApiId — Create a document Creates a new document and returns it. Send field values inside a data object in the request body. **Parameters:** - `data` (object, required): Object containing the field values for the new document **cURL:** ``` curl -X POST \\ 'http://localhost:1337/api/restaurants' \\ -H 'Authorization: Bearer ' \\ -H 'Content-Type: application/json' \\ -d '{ "data": { "Name": "Restaurant D", "Description": [ { "type": "paragraph", "children": [{ "type": "text", "text": "A very short description goes here." }] } ] } }' ``` **JavaScript:** ``` const response = await fetch( 'http://localhost:1337/api/restaurants', { method: 'POST', headers: { Authorization: 'Bearer ', 'Content-Type': 'application/json', }, body: JSON.stringify({ data: { Name: 'Restaurant D', Description: [ { type: 'paragraph', children: [{ type: 'text', text: 'A very short description goes here.' }], }, ], }, }), } ); const data = await response.json(); ``` **Response 200 OK:** ```json { "data": { "documentId": "bw64dnu97i56nq85106yt4du", "Name": "Restaurant D", "Description": [ { "type": "paragraph", "children": [ { "type": "text", "text": "A very short description goes here." } ] } ], "createdAt": "2024-03-05T16:44:47.689Z", "updatedAt": "2024-03-05T16:44:47.689Z", "publishedAt": "2024-03-05T16:44:47.687Z", "locale": "en" }, "meta": {} } ``` ### Update a document {#update} :::note NOTES * Even with the [Internationalization (i18n) plugin](/cms/features/internationalization) installed, it's currently not possible to [update the locale of a document](/cms/api/rest/locale#rest-update). * While updating a document, you can define its relations and their order (see [Managing relations through the REST API](/cms/api/rest/relations) for more details). ::: :::info Dynamic zones Each entry you send for a [dynamic zone](/cms/backend-customization/models#dynamic-zones) must include `__component` with the target component's UID (for example `shared.media`). Strapi uses that field to pick the component schema when you create or update items in the zone; without it, writes can fail validation or return success without changing data. Use the UID shown in the Content-Type Builder for each component in the zone. ::: #### PUT /api/:pluralApiId/:documentId — Update a document Partially updates a document by documentId and returns its value. Send a null value to clear fields. **Parameters:** - `data` (object, required): Object containing the field values to update **cURL:** ``` curl -X PUT \\ 'http://localhost:1337/api/restaurants/hgv1vny5cebq2l3czil1rpb3' \\ -H 'Authorization: Bearer ' \\ -H 'Content-Type: application/json' \\ -d '{ "data": { "Name": "BMK Paris Bamako", "Description": [ { "type": "paragraph", "children": [{ "type": "text", "text": "A very short description goes here." }] } ] } }' ``` **JavaScript:** ``` const response = await fetch( 'http://localhost:1337/api/restaurants/hgv1vny5cebq2l3czil1rpb3', { method: 'PUT', headers: { Authorization: 'Bearer ', 'Content-Type': 'application/json', }, body: JSON.stringify({ data: { Name: 'BMK Paris Bamako', Description: [ { type: 'paragraph', children: [{ type: 'text', text: 'A very short description goes here.' }], }, ], }, }), } ); const data = await response.json(); ``` **Response 200 OK:** ```json { "data": { "id": 9, "documentId": "hgv1vny5cebq2l3czil1rpb3", "Name": "BMK Paris Bamako", "Description": [ { "type": "paragraph", "children": [ { "type": "text", "text": "A very short description goes here." } ] } ], "createdAt": "2024-03-06T13:42:05.098Z", "updatedAt": "2024-03-06T14:16:56.883Z", "publishedAt": "2024-03-06T14:16:56.895Z", "locale": "en" }, "meta": {} } ``` ### Delete a document {#delete} #### DELETE /api/:pluralApiId/:documentId — Delete a document Permanently deletes a document by documentId. This action is irreversible. **Parameters:** - `pluralApiId` (string, required): Plural API ID (e.g. `restaurants`) - `documentId` (string, required): Document ID of the entry to delete **cURL:** ``` curl -X DELETE \\ 'http://localhost:1337/api/restaurants/bw64dnu97i56nq85106yt4du' \\ -H 'Authorization: Bearer ' ``` **JavaScript:** ``` const response = await fetch( 'http://localhost:1337/api/restaurants/bw64dnu97i56nq85106yt4du', { method: 'DELETE', headers: { Authorization: 'Bearer ', }, } ); ``` # Filters Source: https://docs.strapi.io/cms/api/rest/filters # REST API: Filters The REST API filters feature allows filtering query results using operators like `$eq`, `$contains`, and `$between`, with support for complex filtering using `$and`, `$or`, and `$not`, as well as deep filtering across related content. The [REST API](/cms/api/rest) offers the ability to filter results found with its ["Get entries"](/cms/api/rest#get-all) method.
Using optional Strapi features can provide some more filters: - If the [Internationalization (i18n) plugin](/cms/features/internationalization) is enabled on a content-type, it's possible to filter by locale. - If the [Draft & Publish](/cms/features/draft-and-publish) is enabled, it's possible to filter based on a `published` (default) or `draft` status. :::tip Strapi takes advantage of the ability of [the `qs` library](https://github.com/ljharb/qs) to parse nested objects to create more complex queries. Use `qs` directly to generate complex queries instead of creating them manually. Examples in this documentation showcase how you can use `qs`. You can also use the [interactive query builder](/cms/api/rest/interactive-query-builder) if you prefer playing with our online tool instead of generating queries with `qs` on your machine. ::: Queries can accept a `filters` parameter with the following syntax: `GET /api/:pluralApiId?filters[field][operator]=value` The following operators are available: | Operator | Description | | --------------- | ---------------------------------------- | | `$eq` | Equal | | `$eqi` | Equal (case-insensitive) | | `$ne` | Not equal | | `$nei` | Not equal (case-insensitive) | | `$lt` | Less than | | `$lte` | Less than or equal to | | `$gt` | Greater than | | `$gte` | Greater than or equal to | | `$in` | Included in an array | | `$notIn` | Not included in an array | | `$contains` | Contains | | `$notContains` | Does not contain | | `$containsi` | Contains (case-insensitive) | | `$notContainsi` | Does not contain (case-insensitive) | | `$null` | Is null | | `$notNull` | Is not null | | `$between` | Is between | | `$startsWith` | Starts with | | `$startsWithi` | Starts with (case-insensitive) | | `$endsWith` | Ends with | | `$endsWithi` | Ends with (case-insensitive) | | `$or` | Joins the filters in an "or" expression | | `$and` | Joins the filters in an "and" expression | | `$not` | Joins the filters in an "not" expression | When several fields are passed in the `filters` object, they are implicitly combined with `$and` (e.g. `GET /api/restaurants?filters[stars][$gte]=3&filters[open][$eq]=true` only returns restaurants that are open and have at least 3 stars). :::tip `$and`, `$or` and `$not` operators can be nested inside one another. ::: :::caution By default, the filters can only be used from `find` endpoints generated by the Content-type Builder and the CLI. ::: ## Example: Find users having 'John' as a first name #### GET /api/users — Find users having Use the $eq filter operator to find an exact match. **cURL:** ``` GET /api/users?filters[username][$eq]=John ``` **JavaScript:** ``` const qs = require('qs'); const query = qs.stringify({ filters: { username: { $eq: 'John', }, }, }, { encodeValuesOnly: true, // prettify URL }); await request(\`/api/users?\${query}\`); ``` **Response 200 OK:** ```json { "data": [ { "id": 1, "documentId": "znrlzntu9ei5onjvwfaalu2v", "username": "John", "email": "john@test.com", "provider": "local", "confirmed": true, "blocked": false, "createdAt": "2021-12-03T20:08:17.740Z", "updatedAt": "2021-12-03T20:08:17.740Z" } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 1 } } } ``` ## Example: Find multiple restaurants with ids 3, 6,8 #### GET /api/restaurants — Find multiple restaurants with ids 3, 6, 8 Use the $in filter operator with an array of values to find multiple exact values. **cURL:** ``` GET /api/restaurants?filters[id][$in][0]=3&filters[id][$in][1]=6&filters[id][$in][2]=8 ``` **JavaScript:** ``` const qs = require('qs'); const query = qs.stringify({ filters: { id: { $in: [3, 6, 8], }, }, }, { encodeValuesOnly: true, // prettify URL }); await request(\`/api/restaurants?\${query}\`); ``` **Response 200 OK:** ```json { "data": [ { "id": 3, "documentId": "ethwxjxtvuxl89jq720e38uk", "name": "test3" }, { "id": 6, "documentId": "ethwxjxtvuxl89jq720e38uk", "name": "test6" }, { "id": 8, "documentId": "cf07g1dbusqr8mzmlbqvlegx", "name": "test8" } ], "meta": {} } ``` ## Complex filtering #### GET /api/books — Find books with 2 possible dates and a specific author Combine $and and $or operators for complex filtering. **cURL:** ``` GET /api/books?filters[$and][0][$or][0][date][$eq]=2020-01-01&filters[$and][0][$or][1][date][$eq]=2020-01-02&filters[$and][1][author][name][$eq]=Kai%20doe ``` **JavaScript:** ``` const qs = require('qs'); const query = qs.stringify({ filters: { $and: [ { $or: [ { date: { $eq: '2020-01-01', }, }, { date: { $eq: '2020-01-02', }, }, ], }, { author: { name: { $eq: 'Kai doe', }, }, }, ], }, }, { encodeValuesOnly: true, // prettify URL }); await request(\`/api/books?\${query}\`); ``` **Response 200 OK:** ```json { "data": [ { "id": 1, "documentId": "rxngxzclq0zdaqtvz67hj38d", "name": "test1", "date": "2020-01-01" }, { "id": 2, "documentId": "kjkhff4e269a50b4vi16stst", "name": "test2", "date": "2020-01-02" } ], "meta": {} } ``` :::note The response above only contains a book's own attributes. The `author` relation traversed by the `$and` filter is not returned unless requested through the [`populate` parameter](/cms/api/rest/populate-select#population), for example by adding `&populate=author` to the request. ::: ## Deep filtering :::note - Relations, media fields, components, and dynamic zones are not populated by default. Use the `populate` parameter to populate these content structures (see [`populate` documentation](/cms/api/rest/populate-select#population)) - You can filter what you populate, you can also filter nested relations, but you can't use filters for polymorphic content structures (such as media fields and dynamic zones). ::: :::caution Querying your API with deep filters may cause performance issues. If one of your deep filtering queries is too slow, we recommend building a custom route with an optimized version of the query. ::: :::strapi Deep filtering with the various APIs For examples of how to deep filter with the various APIs, please refer to [this blog article](https://strapi.io/blog/deep-filtering-alpha-26). ::: #### GET /api/restaurants — Find restaurants owned by a chef who belongs to a 5-star restaurant Use deep filtering to filter on a relation **cURL:** ``` GET /api/restaurants?filters[chef][restaurants][stars][$eq]=5 ``` **JavaScript:** ``` const qs = require('qs'); const query = qs.stringify({ filters: { chef: { restaurants: { stars: { $eq: 5, }, }, }, }, }, { encodeValuesOnly: true, // prettify URL }); await request(\`/api/restaurants?\${query}\`); ``` **Response 200 OK:** ```json { "data": [ { "id": 1, "documentId": "cvsz61qg33rtyv1qljb1nrtg", "name": "GORDON RAMSAY STEAK", "stars": 5 }, { "id": 2, "documentId": "uh17h7ibw0g8thit6ivi71d8", "name": "GORDON RAMSAY BURGER", "stars": 5 } ], "meta": {} } ``` :::note The response above mirrors the default REST output, which excludes the relations traversed by the filter. Add a [`populate` parameter](/cms/api/rest/populate-select#population) such as `&populate[chef][populate][restaurants]=true` to also return the `chef.restaurants` relation referenced in the filter. ::: # REST API Guides Source: https://docs.strapi.io/cms/api/rest/guides/intro # REST API Guides Explore detailed guides and step-by-step instructions on specific REST API topics, including populate parameters and custom controllers for accessing creator fields. The [REST API reference](/cms/api/rest) documentation is meant to provide a quick reference for all the endpoints and parameters available. ## Guides The following guides, officially maintained by the Strapi Documentation team, cover dedicated topics and provide detailed explanations (guides indicated with 🧠) or step-by-step instructions (guides indicated with 🛠️) for some use cases: - [Understanding populate](/cms/api/rest/guides/understanding-populate): Learn what populating means and how you can use the populate parameter in your REST API queries to add additional fields to your responses. - [How to populate creator fields](/cms/api/rest/guides/populate-creator-fields): Read step-by-step instructions on how to build a custom controller that leverages the populate parameter to add ## Additional resources :::strapi Want to help other users? Some of the additional resources listed in this section have been created for Strapi v4 and might not fully work with Strapi 5. If you want to update one of the following articles for Strapi 5, feel free to [propose an article](https://strapi.io/write-for-the-community) for the Write for the Community program. ::: Additional tutorials and guides can be found in the following blog posts: - [What is a REST API? Beginner’s Guide + Examples Using Strapi](https://strapi.io/blog/what-is-a-rest-api-beginners-guide-examples-using-strapi): Learn the founding principles of REST APIs and how you can use the Strapi REST API. - [Authenticating requests with the REST API](https://strapi.io/blog/guide-on-authenticating-requests-with-the-rest-api): Learn how to authenticate your REST API queries with JSON Web Tokens and API tokens. - [Using Fetch with Strapi](https://strapi.io/blog/mastering-api-requests-using-fetch-with-strapi-content-api): Explore how to use the fetch() method of the Fetch API to interact with Strapi - [Requesting Strapi](https://strapi.io/blog/request-strapi-s-rest-api-behind-a-content-delivery-network-cdn): Learn how to overcome network latency issues when requesting large numbers of media assets by leveraging the usage of a CDN with Strapi # How to populate creator fields Source: https://docs.strapi.io/cms/api/rest/guides/populate-creator-fields # How to populate creator fields such as `createdBy` and `updatedBy` Enable the `populateCreatorFields` option in a content-type schema and create a route middleware to include `createdBy` and `updatedBy` fields in REST API responses. The creator fields `createdBy` and `updatedBy` are removed from the [REST API](/cms/api/rest) response by default. These 2 fields can be returned in the REST API by activating the `populateCreatorFields` parameter at the content-type level. :::note The `populateCreatorFields` property is not available to the GraphQL API. Only the following fields will be populated: `id`, `firstname`, `lastname`, `username`, `preferedLanguage`, `createdAt`, and `updatedAt`. ::: To add `createdBy` and `updatedBy` to the API response: 1. Open the content-type `schema.json` file. 2. Add `"populateCreatorFields": true` to the `options` object: ```json "options": { "draftAndPublish": true, "populateCreatorFields": true }, ``` 3. Save the `schema.json`. 4. Create a new route middleware either using the [generate CLI](/cms/cli.md) or by manually creating a new file in `./src/api/[content-type-name]/middlewares/[your-middleware-name].js` 5. Add the following piece of code, you can modify this example to suit your needs: ```js title="./src/api/test/middlewares/defaultTestPopulate.js" "use strict"; module.exports = (config, { strapi }) => { return async (ctx, next) => { if (!ctx.query.populate) { ctx.query.populate = ["createdBy", "updatedBy"]; } await next(); }; }; ``` 6. Modify your default route factory to enable this middleware on the specific routes you want this population to apply to and replacing the content-type/middleware name with yours: ```js title="./src/api/test/routes/test.js" "use strict"; const { createCoreRouter } = require("@strapi/strapi").factories; module.exports = createCoreRouter("api::test.test", { config: { find: { middlewares: ["api::test.default-test-populate"], }, findOne: { middlewares: ["api::test.default-test-populate"], }, }, }); ``` REST API requests with no `populate` parameter will include the `createdBy` or `updatedBy` fields by default. # Understanding populate Source: https://docs.strapi.io/cms/api/rest/guides/understanding-populate # Understanding the `populate` parameter for the REST API The `populate` parameter in REST API queries includes additional fields, relations, components, and dynamic zones in responses beyond default attributes. Use `populate=*` for all 1-level-deep relations, or explicitly specify fields with nested arrays and fragment syntax for deeper or selective population. :::note Note: Example responses might differ from your experience The content of this page might not be fully up-to-date with Strapi 5 yet: - All the conceptual information and explanations are correct and up-to-date. - However, in the examples, the response content might be slightly different. Examples will be fully up-to-date _after_ the Strapi 5.0.0 (stable version) release and as soon as the [FoodAdvisor](https://github.com/strapi/foodadvisor) example application is upgraded to Strapi 5. However, having slightly different response examples should not prevent you from grasping the essential concepts taught in this page. ::: When querying content-types with Strapi's [REST API](/cms/api/rest), by default, responses only include top-level fields and do not include any relations, media fields, components, or dynamic zones. Populating in the context of the Strapi REST API means including additional content with your response by returning more fields than the ones returned by default. You use the [`populate` parameter](/cms/api/rest/populate-select#population) to achieve this. :::info Throughout this guide, examples are built with real data queried from the server included with the [FoodAdvisor](https://github.com/strapi/foodadvisor) example application. To test examples by yourself, setup FoodAdvisor, start the server in the `/api/` folder, and ensure that proper `find` permissions are given for the queried content-types before sending your queries. ::: The present guide will cover detailed explanations for the following use cases: - populate [all fields and relations, 1 level deep](#populate-all-relations-and-fields-1-level-deep), - populate [some fields and relations, 1 level deep](#populate-1-level-deep-for-specific-relations), - populate [some fields and relations, several levels deep](#populate-several-levels-deep-for-specific-relations), - populate [components](#populate-components), - populate [dynamic zones](#populate-dynamic-zones). :::info Populating several levels deep is often called "deep populate". ::: :::strapi Advanced use case: Populating creator fields In addition to the various ways of using the `populate` parameter in your queries, you can also build a custom controller as a workaround to populate creator fields (e.g., `createdBy` and `updatedBy`). This is explained in the dedicated [How to populate creator fields](/cms/api/rest/guides/populate-creator-fields) guide. ::: ## Populate all relations and fields, 1 level deep You can return all relations, media fields, components and dynamic zones with a single query. For relations, this will only work 1 level deep, to prevent performance issues and long response times. To populate everything 1 level deep, add the `populate=*` parameter to your query. The following diagram compares data returned by the [FoodAdvisor](https://github.com/strapi/foodadvisor) example application with and without populating everything 1 level deep: ![Diagram with populate use cases with FoodAdvisor data ](/img/assets/rest-api/populate-foodadvisor-diagram1.png) Let's compare and explain what happens with and without this query parameter: ### Example: Without `populate` Without the populate parameter, a `GET` request to `/api/articles` only returns the default attributes and does not return any media fields, relations, components or dynamic zones. The following example is the full response for all 4 entries from the `articles` content-types. Notice how the response only includes the `title`, `slug`, `createdAt`, `updatedAt`, `publishedAt`, and `locale` fields, and the field content of the article as handled by the CKEditor plugin (`ckeditor_content`, truncated for brevity): #### GET /api/articles — Without populate Returns only default attributes, without any media fields, relations, components, or dynamic zones. **Request:** ``` GET /api/articles ``` **Response 200 OK:** ```json { "data": [ { "id": 1, "documentId": "t3q2i3v1z2j7o8p6d0o4xxg", "title": "Here's why you have to try basque cuisine, according to a basque chef", "slug": "here-s-why-you-have-to-try-basque-cuisine-according-to-a-basque-chef", "createdAt": "2021-11-09T13:33:19.948Z", "updatedAt": "2023-06-02T10:57:19.584Z", "publishedAt": "2022-09-22T09:30:00.208Z", "locale": "en", "ckeditor_content": "// truncated content" }, { "id": 2, "documentId": "k2r5l0i9g3u2j3b4p7f0sed", "title": "What are chinese hamburgers and why aren't you eating them?", "slug": "what-are-chinese-hamburgers-and-why-aren-t-you-eating-them", "createdAt": "2021-11-11T13:33:19.948Z", "updatedAt": "2023-06-01T14:32:50.984Z", "publishedAt": "2022-09-22T12:36:48.312Z", "locale": "en", "ckeditor_content": "// truncated content" }, { "id": 3, "documentId": "k6m6l9q0n6v9z2m3i0z5jah", "title": "7 Places worth visiting for the food alone", "slug": "7-places-worth-visiting-for-the-food-alone", "createdAt": "2021-11-12T13:33:19.948Z", "updatedAt": "2023-06-02T11:30:00.075Z", "publishedAt": "2023-06-02T11:30:00.075Z", "locale": "en", "ckeditor_content": "// truncated content" }, { "id": 4, "documentId": "d5m4b6z6g5d9e3v1k9n5gbn", "title": "If you don't finish your plate in these countries, you might offend someone", "slug": "if-you-don-t-finish-your-plate-in-these-countries-you-might-offend-someone", "createdAt": "2021-11-15T13:33:19.948Z", "updatedAt": "2023-06-02T10:59:35.148Z", "publishedAt": "2022-09-22T12:35:53.899Z", "locale": "en", "ckeditor_content": "// truncated content" } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 4 } } } ``` ### Example: With `populate=*` With the `populate=*` parameter, a `GET` request to `/api/articles` also returns all media fields, first-level relations, components and dynamic zones. The following example is the full response for the first of all 4 entries from the `articles` content-types (the data from articles with ids 2, 3, and 4 is truncated for brevity). Scroll down to see that the response size is much bigger than without populate. The response now includes additional fields (see highlighted lines) such as: * the `image` media field (which stores all information about the article cover, including all its different formats), * the first-level fields of the `blocks` dynamic zone and the `seo` component, * the `category` relation and its fields, * and even some information about the articles translated in other languages, as shown by the `localizations` object. :::tip To populate deeply nested components, see the [populate components](#populate-components) section. :::
#### GET /api/articles — With populate=* Returns all media fields, first-level relations, components, and dynamic zones. **Request:** ``` GET /api/articles?populate=* ``` **Response 200 OK:** ```json { "data": [ { "id": 1, "title": "Here's why you have to try basque cuisine, according to a basque chef", "slug": "here-s-why-you-have-to-try-basque-cuisine-according-to-a-basque-chef", "createdAt": "2021-11-09T13:33:19.948Z", "updatedAt": "2023-06-02T10:57:19.584Z", "publishedAt": "2022-09-22T09:30:00.208Z", "locale": "en", "ckeditor_content": "// truncated content", "image": { "data": { "id": 12, "documentId": "o5d4b0l4p8l4o4k5n1l3rxa", "name": "Basque dish", "alternativeText": "Basque dish", "caption": "Basque dish", "width": 758, "height": 506, "formats": { "thumbnail": { "name": "thumbnail_https://4d40-2a01-cb00-c8b-1800-7cbb-7da-ea9d-2011.ngrok.io/uploads/basque_cuisine_17fa4567e0.jpeg", "hash": "thumbnail_basque_cuisine_17fa4567e0_f033424240", "ext": ".jpeg", "mime": "image/jpeg", "width": 234, "height": 156, "size": 11.31, "path": null, "url": "/uploads/thumbnail_basque_cuisine_17fa4567e0_f033424240.jpeg" }, "medium": { "name": "medium_https://4d40-2a01-cb00-c8b-1800-7cbb-7da-ea9d-2011.ngrok.io/uploads/basque_cuisine_17fa4567e0.jpeg", "hash": "medium_basque_cuisine_17fa4567e0_f033424240", "ext": ".jpeg", "mime": "image/jpeg", "width": 750, "height": 501, "size": 82.09, "path": null, "url": "/uploads/medium_basque_cuisine_17fa4567e0_f033424240.jpeg" }, "small": { "name": "small_https://4d40-2a01-cb00-c8b-1800-7cbb-7da-ea9d-2011.ngrok.io/uploads/basque_cuisine_17fa4567e0.jpeg", "hash": "small_basque_cuisine_17fa4567e0_f033424240", "ext": ".jpeg", "mime": "image/jpeg", "width": 500, "height": 334, "size": 41.03, "path": null, "url": "/uploads/small_basque_cuisine_17fa4567e0_f033424240.jpeg" } }, "hash": "basque_cuisine_17fa4567e0_f033424240", "ext": ".jpeg", "mime": "image/jpeg", "size": 58.209999999999994, "url": "/uploads/basque_cuisine_17fa4567e0_f033424240.jpeg", "previewUrl": null, "provider": "local", "provider_metadata": null, "createdAt": "2021-11-23T14:05:33.460Z", "updatedAt": "2021-11-23T14:05:46.084Z" } } }, "blocks": [ { "id": 2, "__component": "blocks.related-articles" }, { "id": 2, "documentId": "w8r5k8o8v0t9l9e0d7y6vco", "__component": "blocks.cta-command-line", "theme": "primary", "title": "Want to give a try to a Strapi starter?", "text": "❤️", "commandLine": "git clone https://github.com/strapi/nextjs-corporate-starter.git" } ], "seo": { "id": 1, "documentId": "h7c8d0u3i3q5v1j3j3r4cxf", "metaTitle": "Articles - FoodAdvisor", "metaDescription": "Discover our articles about food, restaurants, bars and more! - FoodAdvisor", "keywords": "food", "metaRobots": null, "structuredData": null, "metaViewport": null, "canonicalURL": null }, "category": { "data": { "id": 4, "documentId": "t1t3d9k6n1k5a6r8l7f8rox", "name": "European", "slug": "european", "createdAt": "2021-11-09T13:33:20.123Z", "updatedAt": "2021-11-09T13:33:20.123Z" } }, "localizations": { "data": [ { "id": 10, "documentId": "h7c8d0u3i3q5v1j3j3r4cxf", "title": "Voici pourquoi il faut essayer la cuisine basque, selon un chef basque", "slug": "voici-pourquoi-il-faut-essayer-la-cuisine-basque-selon-un-chef-basque", "createdAt": "2021-11-18T13:33:19.948Z", "updatedAt": "2023-06-02T10:57:19.606Z", "publishedAt": "2022-09-22T13:00:00.069Z", "locale": "fr-FR", "ckeditor_content": "// truncated content" } ] } } }, { "id": 2, "// truncated content": true }, { "id": 3, "// truncated content": true }, { "id": 4, "// truncated content": true } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 4 } } } ``` ## Populate specific relations and fields You can also populate specific relations and fields, by explicitly defining what to populate. This requires that you know the name of fields and relations to populate. Relations and fields populated this way can be 1 or several levels deep. The following diagram compares data returned by the [FoodAdvisor](https://github.com/strapi/foodadvisor) example application when you populate [1 level deep](#populate-1-level-deep-for-specific-relations) vs. [2 levels deep](#populate-several-levels-deep-for-specific-relations): ![Diagram with populate use cases with FoodAdvisor data ](/img/assets/rest-api/populate-foodadvisor-diagram2.png) **Different populating strategies for similar results** Depending on your content structure, you might get similar data presented in different ways with different queries. For instance, the FoodAdvisor example application includes the article, category, and restaurant content-types that are all in relation to each other in different ways. This means that if you want to get data about the 3 content-types in a single GET request, you have 2 options: - query articles and populate categories, plus populate the nested relation between categories and restaurants ([2 levels deep population](#populate-several-levels-deep-for-specific-relations)) - query categories and populate both articles and restaurants because categories have a 1st level relation with the 2 other content-types ([1 level deep](#populate-1-level-deep-for-specific-relations)) The 2 different strategies are illustrated in the following diagram: ![Diagram with populate use cases with FoodAdvisor data ](/img/assets/rest-api/populate-foodadvisor-diagram3.png)
Populate as an object vs. populate as an array: Using the interactive query builder The syntax for advanced query parameters can be quite complex to build manually. We recommend you use our [interactive query builder](/cms/api/rest/interactive-query-builder) tool to generate the URL. Using this tool, you will write clean and readable requests in a familiar (JavaScript) format, which should help you understand the differences between different queries and different ways of populating. For instance, populating 2 levels deep implies using populate as an object, while populating several relations 1 level deep implies using populate as an array: Populate as an object
(to populate 1 relation several levels deep): ```json { populate: { category: { populate: ['restaurants'], }, }, } ``` Populate as an array
(to populate many relations 1 level deep) ```json { populate: [ 'articles', 'restaurants' ], } ```
### Populate 1 level deep for specific relations You can populate specific relations 1 level deep by using the populate parameter as an array. Since the REST API uses the [LHS bracket notation](https://christiangiacomi.com/posts/rest-design-principles/#lhs-brackets) (i.e., with square brackets `[]`), the parameter syntaxes to populate 1 level deep would look like the following: | How many relations to populate | Syntax example | |-------------------------------|--------------------| | Only 1 relation | `populate[0]=a-relation-name` | | Several relations | `populate[0]=relation-name&populate[1]=another-relation-name&populate[2]=yet-another-relation-name` | Let's compare and explain what happens with and without populating relations 1 level deep when sending queries to the [FoodAdvisor](https://github.com/strapi/foodadvisor) example application: #### Example: Without `populate` Without the populate parameter, a `GET` request to `/api/articles` only returns the default attributes. The following example is the full response for all 4 entries from the `articles` content-type. Notice that the response does not include any media fields, relations, components or dynamic zones:
#### GET /api/articles — Without populate Returns only default attributes for all articles. **Request:** ``` GET /api/articles ``` **Response 200 OK:** ```json { "data": [ { "id": 1, "documentId": "x2m0d7d9o4m2z3u2r2l9yes", "title": "Here's why you have to try basque cuisine, according to a basque chef", "slug": "here-s-why-you-have-to-try-basque-cuisine-according-to-a-basque-chef", "createdAt": "2021-11-09T13:33:19.948Z", "updatedAt": "2023-06-02T10:57:19.584Z", "publishedAt": "2022-09-22T09:30:00.208Z", "locale": "en", "ckeditor_content": "…" }, { "id": 2, "documentId": "k6m6l9q0n6v9z2m3i0z5jah", "title": "What are chinese hamburgers and why aren't you eating them?", "slug": "what-are-chinese-hamburgers-and-why-aren-t-you-eating-them", "createdAt": "2021-11-11T13:33:19.948Z", "updatedAt": "2023-06-01T14:32:50.984Z", "publishedAt": "2022-09-22T12:36:48.312Z", "locale": "en", "ckeditor_content": "…" }, { "id": 3, "documentId": "o5d4b0l4p8l4o4k5n1l3rxa", "title": "7 Places worth visiting for the food alone", "slug": "7-places-worth-visiting-for-the-food-alone", "createdAt": "2021-11-12T13:33:19.948Z", "updatedAt": "2023-06-02T11:30:00.075Z", "publishedAt": "2023-06-02T11:30:00.075Z", "locale": "en", "ckeditor_content": "…" }, { "id": 4, "documentId": "t3q2i3v1z2j7o8p6d0o4xxg", "title": "If you don't finish your plate in these countries, you might offend someone", "slug": "if-you-don-t-finish-your-plate-in-these-countries-you-might-offend-someone", "createdAt": "2021-11-15T13:33:19.948Z", "updatedAt": "2023-06-02T10:59:35.148Z", "publishedAt": "2022-09-22T12:35:53.899Z", "locale": "en", "ckeditor_content": "…" } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 4 } } } ``` #### Example: With `populate[0]=category` With `populate[0]=category` added to the request, we explicitly ask to include some information about `category`, which is a relation field that links the `articles` and the `categories` content-types. The following example is the full response for all 4 entries from the `articles` content-type. Notice that the response now includes additional data with the `category` field for each article (see highlighted lines): #### GET /api/articles — With populate[0]=category Returns articles with their related category data populated. **Request:** ``` GET /api/articles?populate[0]=category ``` **Response 200 OK:** ```json { "data": [ { "id": 1, "documentId": "w8r5k8o8v0t9l9e0d7y6vco", "title": "Here's why you have to try basque cuisine, according to a basque chef", "slug": "here-s-why-you-have-to-try-basque-cuisine-according-to-a-basque-chef", "createdAt": "2021-11-09T13:33:19.948Z", "updatedAt": "2023-06-02T10:57:19.584Z", "publishedAt": "2022-09-22T09:30:00.208Z", "locale": "en", "ckeditor_content": "…", "category": { "data": { "id": 4, "documentId": "u6x8u7o7j5q1l5y3t8j9yxi", "name": "European", "slug": "european", "createdAt": "2021-11-09T13:33:20.123Z", "updatedAt": "2021-11-09T13:33:20.123Z" } } }, { "id": 2, "documentId": "k6m6l9q0n6v9z2m3i0z5jah", "title": "What are chinese hamburgers and why aren't you eating them?", "slug": "what-are-chinese-hamburgers-and-why-aren-t-you-eating-them", "createdAt": "2021-11-11T13:33:19.948Z", "updatedAt": "2023-06-01T14:32:50.984Z", "publishedAt": "2022-09-22T12:36:48.312Z", "locale": "en", "ckeditor_content": "…", "category": { "data": { "id": 13, "documentId": "x2m0d7d9o4m2z3u2r2l9yes", "name": "Chinese", "slug": "chinese", "createdAt": "2021-11-09T13:33:20.123Z", "updatedAt": "2021-11-09T13:33:20.123Z" } } }, { "id": 3, "title": "7 Places worth visiting for the food alone", "slug": "7-places-worth-visiting-for-the-food-alone", "createdAt": "2021-11-12T13:33:19.948Z", "updatedAt": "2023-06-02T11:30:00.075Z", "publishedAt": "2023-06-02T11:30:00.075Z", "locale": "en", "ckeditor_content": "…", "category": { "data": { "id": 3, "documentId": "h7c8d0u3i3q5v1j3j3r4cxf", "name": "International", "slug": "international", "createdAt": "2021-11-09T13:33:20.123Z", "updatedAt": "2021-11-09T13:33:20.123Z" } } }, { "id": 4, "documentId": "t1t3d9k6n1k5a6r8l7f8rox", "title": "If you don't finish your plate in these countries, you might offend someone", "slug": "if-you-don-t-finish-your-plate-in-these-countries-you-might-offend-someone", "createdAt": "2021-11-15T13:33:19.948Z", "updatedAt": "2023-06-02T10:59:35.148Z", "publishedAt": "2022-09-22T12:35:53.899Z", "locale": "en", "ckeditor_content": "…", "category": { "data": { "id": 3, "documentId": "u6x8u7o7j5q1l5y3t8j9yxi", "name": "International", "slug": "international", "createdAt": "2021-11-09T13:33:20.123Z", "updatedAt": "2021-11-09T13:33:20.123Z" } } } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 4 } } } ``` ### Populate several levels deep for specific relations You can also populate specific relations several levels deep. For instance, when you populate a relation which itself populates another relation, you are populating 2 levels deep. Populating 2 levels deep is the example covered in this guide. :::caution There is no limit on the number of levels that can be populated. However, the deeper the populates, the more the request will take time to be performed. ::: Since the REST API uses the [LHS bracket notation](https://christiangiacomi.com/posts/rest-design-principles/#lhs-brackets), (i.e., with square brackets `[]`), for instance if you want to populate a relation nested inside another relation, the parameter syntax would look like the following: `populate[first-level-relation-to-populate][populate][0]=second-level-relation-to-populate` :::tip The syntax for advanced query parameters can be quite complex to build manually. We recommend you use our [interactive query builder](/cms/api/rest/interactive-query-builder) tool to generate the URL. For instance, the `/api/articles?populate[category][populate][0]=restaurants` URL used in the following examples has been generated by converting the following object using our tool: ```json { populate: { category: { populate: ['restaurants'], }, }, } ``` ::: The [FoodAdvisor](https://github.com/strapi/foodadvisor) example application includes various levels of relations between content-types. For instance: - an `article` content-type includes a relation with the `category` content-type, - but a `category` can also be assigned to any `restaurant` content-type. With a single `GET` request to `/api/articles` and the appropriate populate parameters, you can return information about articles, restaurants, and categories simultaneously. Let's compare and explain the responses returned with `populate[0]=category` (1 level deep) and `populate[category][populate][0]=restaurants` (2 levels deep) when sending queries to FoodAdvisor: #### Example: With 1-level deep population When we only populate 1 level deep, asking for the categories associated to articles, we can get the following example response (highlighted lines show the `category` relations field): #### GET /api/articles — With 1-level deep population Populates the category relation 1 level deep. **Request:** ``` GET /api/articles?populate[0]=category ``` **Response 200 OK:** ```json { "data": [ { "id": 1, "documentId": "9ih6hy1bnma3q3066kdwt3", "title": "Here's why you have to try basque cuisine, according to a basque chef", "slug": "here-s-why-you-have-to-try-basque-cuisine-according-to-a-basque-chef", "createdAt": "2021-11-09T13:33:19.948Z", "updatedAt": "2023-06-02T10:57:19.584Z", "publishedAt": "2022-09-22T09:30:00.208Z", "locale": "en", "ckeditor_content": "…", "category": { "data": { "id": 4, "name": "European", "slug": "european", "createdAt": "2021-11-09T13:33:20.123Z", "updatedAt": "2021-11-09T13:33:20.123Z" } } }, { "id": 2, "documentId": "sen6qfgxcac13pwchf8xbu", "title": "What are chinese hamburgers and why aren't you eating them?", "slug": "what-are-chinese-hamburgers-and-why-aren-t-you-eating-them", "createdAt": "2021-11-11T13:33:19.948Z", "updatedAt": "2023-06-01T14:32:50.984Z", "publishedAt": "2022-09-22T12:36:48.312Z", "locale": "en", "ckeditor_content": "…", "category": { "data": { "id": 13, "documentId": "r3rhzcxd7gjx07vkq3pia5", "name": "Chinese", "slug": "chinese", "createdAt": "2021-11-09T13:33:20.123Z", "updatedAt": "2021-11-09T13:33:20.123Z" } } }, { "id": 3, "documentId": "s9uu7rkukhfcsmj2e60b67", "title": "7 Places worth visiting for the food alone", "slug": "7-places-worth-visiting-for-the-food-alone", "createdAt": "2021-11-12T13:33:19.948Z", "updatedAt": "2023-06-02T11:30:00.075Z", "publishedAt": "2023-06-02T11:30:00.075Z", "locale": "en", "ckeditor_content": "…", "category": { "data": { "id": 3, "documentId": "4sevz15w6bdol6y4t8kblk", "name": "International", "slug": "international", "createdAt": "2021-11-09T13:33:20.123Z", "updatedAt": "2021-11-09T13:33:20.123Z" } } }, { "id": 4, "documentId": "iy5ifm3xj8q0t8vlq6l23h", "title": "If you don't finish your plate in these countries, you might offend someone", "slug": "if-you-don-t-finish-your-plate-in-these-countries-you-might-offend-someone", "createdAt": "2021-11-15T13:33:19.948Z", "updatedAt": "2023-06-02T10:59:35.148Z", "publishedAt": "2022-09-22T12:35:53.899Z", "locale": "en", "ckeditor_content": "…", "category": { "data": { "id": 3, "documentId": "0eor603u8qej933maphdv3", "name": "International", "slug": "international", "createdAt": "2021-11-09T13:33:20.123Z", "updatedAt": "2021-11-09T13:33:20.123Z" } } } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 4 } } } ``` #### Example: With 2-level deep population When we populate 2 levels deep, asking for the categories associated to articles, but also for restaurants associated to these categories, we can get the following example response. Notice that we now have the `restaurants` relation field included with the response inside the `category` relation (see highlighted lines): #### GET /api/articles — With 2-level deep population Populates the category relation and the nested restaurants relation 2 levels deep. **Request:** ``` GET /api/articles?populate[category][populate][0]=restaurants ``` **Response 200 OK:** ```json { "data": [ { "id": 1, "documentId": "iy5ifm3xj8q0t8vlq6l23h", "title": "Here's why you have to try basque cuisine, according to a basque chef", "slug": "here-s-why-you-have-to-try-basque-cuisine-according-to-a-basque-chef", "createdAt": "2021-11-09T13:33:19.948Z", "updatedAt": "2023-06-02T10:57:19.584Z", "publishedAt": "2022-09-22T09:30:00.208Z", "locale": "en", "ckeditor_content": "…", "category": { "data": { "id": 4, "name": "European", "slug": "european", "createdAt": "2021-11-09T13:33:20.123Z", "updatedAt": "2021-11-09T13:33:20.123Z", "restaurants": { "data": [ { "id": 1, "documentId": "ozlqrdxpnjb7wtvf6lp74v", "name": "Mint Lounge", "slug": "mint-lounge", "price": "p3", "createdAt": "2021-11-09T14:07:47.125Z", "updatedAt": "2021-11-23T16:41:30.504Z", "publishedAt": "2021-11-23T16:41:30.501Z", "locale": "en" }, { "id": 9, "// truncated content": true }, { "id": 10, "// truncated content": true }, { "id": 12, "// truncated content": true }, { "id": 21, "// truncated content": true }, { "id": 26, "// truncated content": true } ] } } } }, { "id": 2, "// truncated content": true }, { "id": 3, "// truncated content": true }, { "id": 4, "// truncated content": true } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 4 } } } ``` ### Populate components Components and dynamic zones are not included in responses by default and you need to explicitly populate each dynamic zones, components, and their nested components. Since the REST API uses the [LHS bracket notation](https://christiangiacomi.com/posts/rest-design-principles/#lhs-brackets), (i.e., with square brackets `[]`), you need to pass all elements in a `populate` array. Nested fields can also be passed, and the parameter syntax could look like the following: `populate[0]=a-first-field&populate[1]=a-second-field&populate[2]=a-third-field&populate[3]=a-third-field.a-nested-field&populate[4]=a-third-field.a-nested-component.a-nested-field-within-the-component` :::tip The syntax for advanced query parameters can be quite complex to build manually. We recommend you use our [interactive query builder](/cms/api/rest/interactive-query-builder) tool to generate the URL. For instance, the `/api/articles?populate[0]=seo&populate[1]=seo.metaSocial&populate[2]=seo.metaSocial.image` URL used in the following examples has been generated by converting the following object using our tool: ```json { populate: [ 'seoData', 'seoData.sharedImage', 'seoData.sharedImage.media', ], }, ``` ::: The [FoodAdvisor](https://github.com/strapi/foodadvisor) example application includes various components and even components nested inside other components. For instance: - an `article` content-type includes a `seo` component , - the `seo` component includes a nested, repeatable `metaSocial` component , - and the `metaSocial` component itself has several fields, including an `image` media field . ![FoodAdvisor's SEO component structure in the Content-Type Builder](/img/assets/rest-api/ctb-article-components-structure.png) By default, none of these fields or components are included in the response of a `GET` request to `/api/articles`. But with the appropriate populate parameters, you can return all of them in a single request. Let's compare and explain the responses returned with `populate[0]=seo` (1st level component) and `populate[0]=seo&populate[1]=seo.metaSocial` (2nd level component nested within the 1st level component): #### Example: Only 1st level component When we only populate the `seo` component, we go only 1 level deep, and we can get the following example response. Highlighted lines show the `seo` component. Notice there's no mention of the `metaSocial` component nested within the `seo` component: #### GET /api/articles — Only 1st level component Populates only the seo component, 1 level deep. **Request:** ``` GET /api/articles?populate[0]=seo ``` **Response 200 OK:** ```json { "data": [ { "id": 1, "documentId": "md60m5cy3dula5g87x1uar", "title": "Here's why you have to try basque cuisine, according to a basque chef", "slug": "here-s-why-you-have-to-try-basque-cuisine-according-to-a-basque-chef", "createdAt": "2021-11-09T13:33:19.948Z", "updatedAt": "2023-06-02T10:57:19.584Z", "publishedAt": "2022-09-22T09:30:00.208Z", "locale": "en", "ckeditor_content": "…", "seo": { "id": 1, "documentId": "kqcwhq6hes25kt9ebj8x7j", "metaTitle": "Articles - FoodAdvisor", "metaDescription": "Discover our articles about food, restaurants, bars and more! - FoodAdvisor", "keywords": "food", "metaRobots": null, "structuredData": null, "metaViewport": null, "canonicalURL": null } }, { "id": 2, "// truncated content": true }, { "id": 3, "// truncated content": true }, { "id": 4, "// truncated content": true } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 4 } } } ``` #### Example: 1st level and 2nd level component When we populate 2 levels deep, asking both for the `seo` component and the `metaSocial` component nested inside `seo`, we can get the following example response. Notice that we now have the `metaSocial` component-related data included with the response (see highlighted lines): #### GET /api/articles — 1st level and 2nd level component Populates the seo component and the nested metaSocial component. **Request:** ``` GET /api/articles?populate[0]=seo&populate[1]=seo.metaSocial ``` **Response 200 OK:** ```json { "data": [ { "id": 1, "documentId": "c2imt19iywk27hl2ftph7s", "title": "Here's why you have to try basque cuisine, according to a basque chef", "slug": "here-s-why-you-have-to-try-basque-cuisine-according-to-a-basque-chef", "createdAt": "2021-11-09T13:33:19.948Z", "updatedAt": "2023-06-02T10:57:19.584Z", "publishedAt": "2022-09-22T09:30:00.208Z", "locale": "en", "ckeditor_content": "…", "seo": { "id": 1, "documentId": "e8cnux5ejxyqrejd5addfv", "metaTitle": "Articles - FoodAdvisor", "metaDescription": "Discover our articles about food, restaurants, bars and more! - FoodAdvisor", "keywords": "food", "metaRobots": null, "structuredData": null, "metaViewport": null, "canonicalURL": null, "metaSocial": [ { "id": 1, "documentId": "ks7xsp9fewoi0qljcz9qa0", "socialNetwork": "Facebook", "title": "Browse our best articles about food and restaurants ", "description": "Discover our articles about food, restaurants, bars and more!" } ] } }, { "id": 2, "// truncated content": true }, { "id": 3, "// truncated content": true }, { "id": 4, "// truncated content": true } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 4 } } } ``` ### Populate dynamic zones Dynamic zones are highly dynamic content structures by essence. When querying dynamic zones, standard populate parameters (such as `populate[0]=dynamic-zone-name` or `populate=*`) will only fetch the default scalar fields (e.g., strings, numbers, booleans) of the components within the dynamic zone. By default, they will **not** populate nested relations, media fields, or nested components inside those components. To retrieve component-specific nested relations, media fields, or components within a dynamic zone, you must define per-component populate queries using the `on` property (fragment population syntax). This is because different components in a dynamic zone can have completely different structures, and require their own unique nested queries. For instance, in the [FoodAdvisor](https://github.com/strapi/foodadvisor) example application: - A `blocks` dynamic zone exists on the `article` content-type . - The dynamic zone includes 3 different components: `relatedArticles` , `faq` , and `CtaCommandLine` . All components have a different content structure containing various fields. - The `relatedArticles` component has an `articles` relation with the article content-type. ![FoodAdvisor's 'blocks' dynamic zone structure in the Content-Type Builder](/img/assets/rest-api/ctb-blocks-dynamic-zone-structure-2.png) By default, none of the deeply nested fields or relations are included in the response of a `GET` request to `/api/articles`. With the appropriate populate parameters and by applying a detailed population strategy using the fragment `on` property, you can return precisely the data you need. :::tip The syntax for advanced query parameters can be quite complex to build manually. We recommend you use our [interactive query builder](/cms/api/rest/interactive-query-builder) tool to generate the URL. For instance, the `/api/articles?populate[blocks][on][blocks.related-articles][populate][articles][populate][0]=image&populate[blocks][on][blocks.cta-command-line][populate]=*` URL used in the following example has been generated by converting the following object using our tool: ```json { populate: { blocks: { // asking to populate the blocks dynamic zone on: { // using a detailed population strategy to explicitly define what you want 'blocks.related-articles': { populate: { 'articles': { populate: ['image'] } } }, 'blocks.cta-command-line': { populate: '*' } }, }, }, } ``` ::: Let's compare and explain the responses returned with some examples of a shared population strategy and a detailed population strategy: #### Example When we populate the `blocks` dynamic zone, we explicitly define which data to populate. In the following example response, highlighted lines show that: - We deeply populate the `articles` relation of the `relatedArticles` component, and even the `image` media field of the related article using fragment (`on`) population. - But because we have only asked to populate everything for the `CtaCommandLine` component and have not defined anything for the `faq` component, no data from the `faq` component is returned. #### GET /api/articles — Detailed population strategy for dynamic zones Populates the blocks dynamic zone using a detailed per-component population strategy. **Request:** ``` GET /api/articles?populate[blocks][on][blocks.related-articles][populate][articles][populate][0]=image&populate[blocks][on][blocks.cta-command-line][populate]=* ``` **Response 200 OK:** ```json { "data": [ { "id": 1, "documentId": "it9bbhcgc6mcfsqas7h1dp", "title": "Here's why you have to try basque cuisine, according to a basque chef", "slug": "here-s-why-you-have-to-try-basque-cuisine-according-to-a-basque-chef", "createdAt": "2021-11-09T13:33:19.948Z", "updatedAt": "2023-06-02T10:57:19.584Z", "publishedAt": "2022-09-22T09:30:00.208Z", "locale": "en", "ckeditor_content": "// truncated content", "blocks": [ { "id": 2, "documentId": "e8cnux5ejxyqrejd5addfv", "__component": "blocks.related-articles", "articles": { "data": [ { "id": 2, "documentId": "wkgojrcg5bkz8teqx1foz7", "title": "What are chinese hamburgers and why aren't you eating them?", "slug": "what-are-chinese-hamburgers-and-why-aren-t-you-eating-them", "createdAt": "2021-11-11T13:33:19.948Z", "updatedAt": "2023-06-01T14:32:50.984Z", "publishedAt": "2022-09-22T12:36:48.312Z", "locale": "en", "ckeditor_content": "// truncated content", "image": { "data": { "// …": true } } } }, { "id": 3, "// …": true }, { "id": 4, "// …": true } ] } }, { "id": 2, "__component": "blocks.cta-command-line", "theme": "primary", "title": "Want to give a try to a Strapi starter?", "text": "❤️", "commandLine": "git clone https://github.com/strapi/nextjs-corporate-starter.git" } ] }, { "id": 2, "// …": true }, { "id": 3, "documentId": "z5jnfvyuj07fogzh1kcbd3", "title": "7 Places worth visiting for the food alone", "slug": "7-places-worth-visiting-for-the-food-alone", "createdAt": "2021-11-12T13:33:19.948Z", "updatedAt": "2023-06-02T11:30:00.075Z", "publishedAt": "2023-06-02T11:30:00.075Z", "locale": "en", "ckeditor_content": "// truncated content", "blocks": [ { "id": 1, "documentId": "ks7xsp9fewoi0qljcz9qa0", "__component": "blocks.related-articles", "articles": { "// …": true } }, { "id": 1, "documentId": "c2imt19iywk27hl2ftph7s", "__component": "blocks.cta-command-line", "theme": "secondary", "title": "Want to give it a try with a brand new project?", "text": "Up & running in seconds 🚀", "commandLine": "npx create-strapi-app my-project --quickstart" } ] }, { "id": 4, "// …": true } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 4 } } } ``` :::tip Avoid over-populating in production Using `populate=*` or deep population plugins can create unpredictable, costly database queries. In production, always populate explicitly and limit depth to 2-3 levels. Consider using route-level middlewares to centralize population logic. See [Building High-Performance Strapi Applications](https://strapi.io/blog/building-high-performance-strapi-applications-common-pitfalls-and-best-practices) on the Strapi blog. ::: # Interactive Query Builder Source: https://docs.strapi.io/cms/api/rest/interactive-query-builder # Build your query URL with Strapi's interactive tool An interactive query builder tool that automatically generates REST API query URLs from your endpoint and parameters, powered by the `qs` library to handle complex nested queries. A wide range of parameters can be used and combined to query your content with the [REST API](/cms/api/rest), which can result in long and complex query URLs. Strapi's codebase uses [the `qs` library](https://github.com/ljharb/qs) to parse and stringify nested JavaScript objects. It's recommended to use `qs` directly to generate complex query URLs instead of creating them manually. You can use the following interactive query builder tool to generate query URLs automatically: 1. Replace the values in the _Endpoint_ and _Endpoint Query Parameters_ fields with content that fits your needs. 2. Click the **Copy to clipboard** button to copy the automatically generated _Query String URL_ which is updated as you type. :::info Parameters usage Please refer to the [REST API parameters table](/cms/api/rest/parameters) and read the corresponding parameters documentation pages to better understand parameters usage. :::


:::note The default endpoint path is prefixed with `/api/` and should be kept as-is unless you configured a different API prefix using [the `rest.prefix` API configuration option](/cms/configurations/api).
For instance, to query the `books` collection type using the default API prefix, type `/api/books` in the _Endpoint_ field. ::: :::caution Disclaimer The `qs` library and the interactive query builder provided on this page: - might not detect all syntax errors, - are not aware of the parameters and values available in a Strapi project, - and do not provide autocomplete features. Currently, these tools are only provided to transform the JavaScript object in an inline query string URL. Using the generated query URL does not guarantee that proper results will get returned with your API. ::: # Locale Source: https://docs.strapi.io/cms/api/rest/locale # REST API: `locale` The `locale` REST API parameter retrieves and manages documents in specific languages, defaulting to the application's default locale. Use it to fetch, create, update, and delete locale-specific versions of documents in both collection and single types. The [Internationalization (i18n) feature](/cms/features/internationalization) adds new abilities to the [REST API](/cms/api/rest). :::prerequisites To work with API content for a locale, please ensure the locale has been already [added to Strapi in the admin panel](/cms/features/internationalization#settings). ::: The `locale` [API parameter](/cms/api/rest/parameters) can be used to work with documents only for a specified locale. `locale` takes a locale code as a value (see [full list of available locales](https://github.com/strapi/strapi/blob/main/packages/plugins/i18n/server/src/constants/iso-locales.json)). :::tip If the `locale` parameter is not defined, it will be set to the default locale. `en` is the default locale when a new Strapi project is created, but another locale can be [set as the default locale](/cms/features/internationalization#settings) in the admin panel. For instance, by default, a GET request to `/api/restaurants` will return the same response as a request to `/api/restaurants?locale=en`. ::: The following table lists the new possible use cases added by i18n to the REST API and gives syntax examples (you can click on requests to jump to the corresponding section with more details): | Use case | Syntax example
and link for more information | |---------|-------| | Get all documents in a specific locale | [`GET /api/restaurants?locale=fr`](#rest-get-all) | | Get a specific locale version for a document | [`GET /api/restaurants/abcdefghijklmno456?locale=fr`](#get-one-collection-type) | | Create a new document for the default locale | [`POST /api/restaurants`](#rest-create-default-locale)
+ pass attributes in the request body | | Create a new document for a specific locale | [`POST /api/restaurants?locale=fr`](#rest-create-specific-locale)
+ pass attributes in the request body | | Create a new, or update an existing, locale version for an existing document | [`PUT /api/restaurants/abcdefghijklmno456?locale=fr`](#rest-put-collection-type)
+ pass attributes in the request body | | Delete a specific locale version of a document | [`DELETE /api/restaurants/abcdefghijklmno456?locale=fr`](#rest-delete-collection-type) | | Use case | Syntax example
and link for more information | |----------------------------------------------|--------------------------------------------------| | Get a specific locale version for a document | [`GET /api/homepage?locale=fr`](#get-one-single-type) | | Create a new, or update an existing, locale version for an existing document | [`PUT /api/homepage?locale=fr`](#rest-put-single-type)
+ pass attributes in the request body | | Delete a specific locale version of a document | [`DELETE /api/homepage?locale=fr`](#rest-delete-single-type) | ### `GET` Get all documents in a specific locale {#rest-get-all} #### GET /api/restaurants?locale=fr — Get all documents in a specific locale Returns all documents for a given locale. **Request:** ``` GET http://localhost:1337/api/restaurants?locale=fr ``` **Response 200 OK:** ```json { "data": [ { "id": 5, "documentId": "h90lgohlzfpjf3bvan72mzll", "Title": "Meilleures pizzas", "Body": [ { "type": "paragraph", "children": [ { "type": "text", "text": "On déguste les meilleures pizzas de la ville à la Pizzeria Arrivederci." } ] } ], "createdAt": "2024-03-06T22:08:59.643Z", "updatedAt": "2024-03-06T22:10:21.127Z", "publishedAt": "2024-03-06T22:10:21.130Z", "locale": "fr" } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 1 } } } ``` ### `GET` Get a document in a specific locale {#rest-get} To get a specific document in a given locale, add the `locale` parameter to the query: | Use case | Syntax format and link for more information | | -------------------- | ---------------------------------------------------------------------------------------------- | | In a collection type | [`GET /api/content-type-plural-name/document-id?locale=locale-code`](#get-one-collection-type) | | In a single type | [`GET /api/content-type-singular-name?locale=locale-code`](#get-one-single-type) | #### Collection types {#get-one-collection-type} To get a specific document in a collection type in a given locale, add the `locale` parameter to the query, after the `documentId`: #### GET /api/restaurants/:documentId?locale=fr — Get a document in a specific locale (collection type) Returns a specific document in a collection type for a given locale. **Request:** ``` GET /api/restaurants/lr5wju2og49bf820kj9kz8c3?locale=fr ``` **Response 200 OK:** ```json { "data": [ { "id": 22, "documentId": "lr5wju2og49bf820kj9kz8c3", "Name": "Biscotte Restaurant", "Description": [ { "type": "paragraph", "children": [ { "type": "text", "text": "Bienvenue au restaurant Biscotte! Le Restaurant Biscotte propose une cuisine à base de produits frais et de qualité, souvent locaux, biologiques lorsque cela est possible, et toujours produits par des producteurs passionnés." } ] } ], "locale": "fr" } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 3 } } } ``` #### Single types {#get-one-single-type} To get a specific single type document in a given locale, add the `locale` parameter to the query, after the single type name: #### GET /api/homepage?locale=fr — Get a document in a specific locale (single type) Returns a specific single type document for a given locale. **Request:** ``` GET /api/homepage?locale=fr ``` **Response 200 OK:** ```json { "data": { "id": 10, "documentId": "ukbpbnu8kbutpn98rsanyi50", "Title": "Page d'accueil", "Body": null, "createdAt": "2024-03-07T13:28:26.349Z", "updatedAt": "2024-03-07T13:28:26.349Z", "publishedAt": "2024-03-07T13:28:26.353Z", "locale": "fr" }, "meta": {} } ``` ### `POST` Create a new localized document for a collection type {#rest-create} To create a localized document from scratch, send a POST request to the Content API. Depending on whether you want to create it for the default locale or for another locale, you might need to pass the `locale` parameter in the query. | Use case | Syntax format and link for more information | | ----------------------------- | --------------------------------------------------------------------------------------- | | Create for the default locale | [`POST /api/content-type-plural-name`](#rest-create-default-locale) | | Create for a specific locale | [`POST /api/content-type-plural-name?locale=fr`](#rest-create-specific-locale) #### For the default locale {#rest-create-default-locale} If no locale has been passed in the request body, the document is created using the default locale for the application: #### POST /api/restaurants — Create a document for the default locale Creates a new document using the default locale. **Request:** ``` POST http://localhost:1337/api/restaurants { "data": { "Name": "Oplato" } } ``` **Response 200 OK:** ```json { "data": { "id": 13, "documentId": "jae8klabhuucbkgfe2xxc5dj", "Name": "Oplato", "Description": null, "createdAt": "2024-03-06T22:19:54.646Z", "updatedAt": "2024-03-06T22:19:54.646Z", "publishedAt": "2024-03-06T22:19:54.649Z", "locale": "en" }, "meta": {} } ``` #### For a specific locale {#rest-create-specific-locale} To create a localized entry for a locale different from the default one, add the `locale` parameter to the query URL of the POST request: #### POST /api/restaurants?locale=fr — Create a document for a specific locale Creates a new document for a specified locale. **Request:** ``` POST http://localhost:1337/api/restaurants?locale=fr { "data": { "Name": "She's Cake" } } ``` **Response 200 OK:** ```json { "data": { "id": 15, "documentId": "ldcmn698iams5nuaehj69j5o", "Name": "She's Cake", "Description": null, "createdAt": "2024-03-06T22:21:18.373Z", "updatedAt": "2024-03-06T22:21:18.373Z", "publishedAt": "2024-03-06T22:21:18.378Z", "locale": "fr" }, "meta": {} } ``` ### `PUT` Create a new, or update an existing, locale version for an existing document {#rest-update} With `PUT` requests sent to an existing document, you can: - create another locale version of the document, - or update an existing locale version of the document. Send the `PUT` request to the appropriate URL, adding the `locale=your-locale-code` parameter to the query URL and passing attributes in a `data` object in the request's body: | Use case | Syntax format and link for more information | | -------------------- | --------------------------------------------------------------------------------------- | | In a collection type | [`PUT /api/content-type-plural-name/document-id?locale=locale-code`](#rest-put-collection-type) | | In a single type | [`PUT /api/content-type-singular-name?locale=locale-code`](#rest-put-single-type) | :::caution When creating a localization for existing localized entries, the body of the request can only accept localized fields. ::: :::tip The Content-Type should have the [`createLocalization` permission](/cms/features/rbac#collection-and-single-types) enabled, otherwise the request will return a `403: Forbidden` status. ::: :::note It is not possible to change the locale of an existing localized entry. When updating a localized entry, if you set a `locale` attribute in the request body it will be ignored. ::: #### In a collection type {#rest-put-collection-type} To create a new locale for an existing document in a collection type, add the `locale` parameter to the query, after the `documentId`, and pass data to the request's body: #### PUT /api/restaurants/:documentId?locale=fr — Create or update a locale version (collection type) Creates a French locale for an existing restaurant, or updates it if it already exists. **Request:** ``` PUT http://localhost:1337/api/restaurants/lr5wju2og49bf820kj9kz8c3?locale=fr { "data": { "Name": "She's Cake in French" } } ``` **Response 200 OK:** ```json { "data": { "id": 19, "documentId": "lr5wju2og49bf820kj9kz8c3", "Name": "She's Cake in French", "Description": null, "createdAt": "2024-03-07T12:13:09.551Z", "updatedAt": "2024-03-07T12:13:09.551Z", "publishedAt": "2024-03-07T12:13:09.554Z", "locale": "fr" }, "meta": {} } ``` #### In a single type {#rest-put-single-type} To create a new locale for an existing single type document, add the `locale` parameter to the query, after the single type name, and pass data to the request's body: #### PUT /api/homepage?locale=fr — Create or update a locale version (single type) Creates a French locale for an existing Homepage single type, or updates it if it already exists. **Request:** ``` PUT http://localhost:1337/api/homepage?locale=fr { "data": { "Title": "Page d'accueil" } } ``` **Response 200 OK:** ```json { "data": { "id": 10, "documentId": "ukbpbnu8kbutpn98rsanyi50", "Title": "Page d'accueil", "Body": null, "createdAt": "2024-03-07T13:28:26.349Z", "updatedAt": "2024-03-07T13:28:26.349Z", "publishedAt": "2024-03-07T13:28:26.353Z", "locale": "fr" }, "meta": {} } ```
### `DELETE` Delete a locale version of a document {#rest-delete} To delete a locale version of a document, send a `DELETE` request with the appropriate `locale` parameter. `DELETE` requests only send a 204 HTTP status code on success and do not return any data in the response body. #### In a collection type {#rest-delete-collection-type} To delete only a specific locale version of a document in a collection type, add the `locale` parameter to the query after the `documentId`: #### DELETE /api/restaurants/:documentId?locale=fr — Delete a locale version (collection type) Deletes a specific locale version of a document in a collection type. **Request:** ``` DELETE /api/restaurants/abcdefghijklmno456?locale=fr ``` **Response 200 No Content:** ```json (response body) ``` #### In a single type {#rest-delete-single-type} To delete only a specific locale version of a single type document, add the `locale` parameter to the query after the single type name: #### DELETE /api/homepage?locale=fr — Delete a locale version (single type) Deletes a specific locale version of a single type document. **Request:** ``` DELETE /api/homepage?locale=fr ``` **Response 200 No Content:** ```json (response body) ``` # Parameters Source: https://docs.strapi.io/cms/api/rest/parameters # REST API parameters REST API parameters filter, sort, paginate, and select fields and relations in Strapi queries. Use `filters`, `locale`, `populate`, `sort`, and `pagination` to refine your content requests. API parameters can be used with the [REST API](/cms/api/rest) to filter, sort, and paginate results and to select fields and relations to populate. Additionally, specific parameters related to optional Strapi features can be used, like the publication state and locale of a content-type. The following API parameters are available: | Operator | Type | Description | | ------------------ | ------------- | ----------------------------------------------------- | | `filters` | Object | [Filter the response](/cms/api/rest/filters) | | `locale` | String | [Select a locale](/cms/api/rest/locale) | | `status` | String | [Select the Draft & Publish status](/cms/api/rest/status) | | `publicationFilter` | String | [Select a derived Draft & Publish cohort](/cms/api/rest/publication-filter) | | `populate` | String or Object | [Populate relations, components, or dynamic zones](/cms/api/rest/populate-select#population) | | `fields` | Array | [Select only specific fields to display](/cms/api/rest/populate-select#field-selection) | | `sort` | String or Array | [Sort the response](/cms/api/rest/sort-pagination.md#sorting) | | `pagination` | Object | [Page through entries](/cms/api/rest/sort-pagination.md#pagination) | :::note Long bracket-encoded lists in a parameter (for example `populate` or `fields`) are limited by [`arrayLimit` on `strapi::query`](/cms/configurations/middlewares#query). See [Population](/cms/api/rest/populate-select#population). ::: Query parameters use the [LHS bracket syntax](https://christiangiacomi.com/posts/rest-design-principles/#lhs-brackets) (i.e. they are encoded using square brackets `[]`). :::tip A wide range of REST API parameters can be used and combined to query your content, which can result in long and complex query URLs.
👉 You can use Strapi's [interactive query builder](/cms/api/rest/interactive-query-builder) tool to build query URLs more conveniently. 🤗 ::: # Populate and Select Source: https://docs.strapi.io/cms/api/rest/populate-select # REST API: Population & Field Selection Use the `populate` parameter to include relations, media fields, components, and dynamic zones in REST API responses. Use the `fields` parameter to return only specific fields. The [REST API](/cms/api/rest) by default does not populate any relations, media fields, components, or dynamic zones. Use the [`populate` parameter](#population) to populate specific fields. Use the [`fields` parameter](#field-selection) to return only specific fields with the query results. :::tip Strapi takes advantage of the ability of [the `qs` library](https://github.com/ljharb/qs) to parse nested objects to create more complex queries. Use `qs` directly to generate complex queries instead of creating them manually. Examples in this documentation showcase how you can use `qs`. You can also use the [interactive query builder](/cms/api/rest/interactive-query-builder) if you prefer playing with our online tool instead of generating queries with `qs` on your machine. ::: ## Field selection Queries can accept a `fields` parameter to select only some fields. By default, the REST API only returns the following [types of fields](/cms/backend-customization/models#model-attributes): - string types: `string`, `text`, `richtext`, `enumeration`, `email`, `password`, and `uid`, - date types: `date`, `time`, `datetime`, and `timestamp`, - number types: `integer`, `biginteger`, `float`, and `decimal`, - generic types: `boolean`, `array`, and `JSON`. | Use case | Example parameter syntax | |-----------------------|---------------------------------------| | Select a single field | `fields=name` | | Select multiple fields| `fields[0]=name&fields[1]=description`| :::note Field selection does not work on relational, media, component, or dynamic zone fields. To populate these fields, use the [`populate` parameter](#population). ::: #### GET /api/restaurants — Return only name and description fields Use the fields parameter to select only specific fields in the response. **URL:** ``` GET /api/restaurants?fields[0]=name&fields[1]=description ``` **JavaScript:** ``` const qs = require('qs'); const query = qs.stringify( { fields: ['name', 'description'], }, { encodeValuesOnly: true, // prettify URL } ); await request(\`/api/users?\${query}\`); ``` **Response 200 OK:** ```json { "data": [ { "id": 4, "Name": "Pizzeria Arrivederci", "Description": [ { "type": "paragraph", "children": [ { "type": "text", "text": "Specialized in pizza, we invite you to rediscover our classics, such as 4 Formaggi or Calzone, and our original creations such as Do Luigi or Nduja." } ] } ], "documentId": "lr5wju2og49bf820kj9kz8c3" } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 4 } } } ``` ## Population The REST API by default does not populate any type of fields, so it will not populate relations, media fields, components, or dynamic zones unless you pass a `populate` parameter to populate various field types. Populated relations always return full objects; the REST API currently cannot return just an array of IDs. :::prerequisites The `find` permission must be enabled for the content-types that are being populated. If a role does not have access to a content-type, the content-type will not be populated (see [Users & Permissions](/cms/features/users-permissions#editing-a-role) for additional information on how to enable `find` permissions for content-types). ::: You can use the `populate` parameter alone or [in combination with multiple operators](#combining-population-with-other-operators) for more control over the population. :::caution `populate=deep` plugins are [not recommended in Strapi](https://support.strapi.io/articles/8544110758-why-populate-deep-plugins-are-not-recommended-in-strapi). ::: :::note Large `populate` lists in the query string (many `populate[0]`, `populate[1]`, … entries) are bounded by the query parser `arrayLimit` (default: `100`). To allow a longer list, raise `arrayLimit` on the [`strapi::query` middleware](/cms/configurations/middlewares#query). Higher values increase parsing cost per request. ::: The following table lists populate use cases with example syntax. Each row links to the Understanding populate guide for details: | Use case | Example parameter syntax | Detailed explanations to read | |-----------| ---------------|-----------------------| | Populate everything, 1 level deep, including media fields, relations, components, and dynamic zones | `populate=*`| [Populate all relations and fields, 1 level deep](/cms/api/rest/guides/understanding-populate#populate-all-relations-and-fields-1-level-deep) | | Populate one relation,
1 level deep | `populate=a-relation-name`| [Populate 1 level deep for specific relations](/cms/api/rest/guides/understanding-populate#populate-1-level-deep-for-specific-relations) | | Populate several relations,
1 level deep | `populate[0]=relation-name&populate[1]=another-relation-name&populate[2]=yet-another-relation-name`| [Populate 1 level deep for specific relations](/cms/api/rest/guides/understanding-populate#populate-1-level-deep-for-specific-relations) | | Populate some relations, several levels deep | `populate[root-relation-name][populate][0]=nested-relation-name`| [Populate several levels deep for specific relations](/cms/api/rest/guides/understanding-populate#populate-several-levels-deep-for-specific-relations) | | Populate a component | `populate[0]=component-name`| [Populate components](/cms/api/rest/guides/understanding-populate#populate-components) | | Populate a component and one of its nested components | `populate[0]=component-name&populate[1]=component-name.nested-component-name`| [Populate components](/cms/api/rest/guides/understanding-populate#populate-components) | | Populate a dynamic zone (only its first-level scalar fields) | `populate[0]=dynamic-zone-name`| [Populate dynamic zones](/cms/api/rest/guides/understanding-populate#populate-dynamic-zones) | | Populate a dynamic zone, including component-specific fields, nested components, and relations | `populate[dynamic-zone-name][on][component-category.component-name][populate][relation-name][populate][0]=field-name`| [Populate dynamic zones](/cms/api/rest/guides/understanding-populate#populate-dynamic-zones) | :::tip To build complex queries with multiple-level population, use the [interactive query builder](/cms/api/rest/interactive-query-builder) tool. For more detailed explanations and examples, see the [REST API guides](/cms/api/rest/guides/intro). ::: ### Combining population with other operators You can combine the `populate` operator with other operators such as [field selection](/cms/api/rest/populate-select#field-selection), [filters](/cms/api/rest/filters), and [sort](/cms/api/rest/sort-pagination) in the population queries. :::note Top-level pagination parameters (e.g., `pagination[page]` and `pagination[pageSize]`) work alongside `populate` to paginate the main query results. However, you cannot apply pagination parameters directly to populated relations to limit the number of related entries returned within each result (nested pagination on relations is not supported in the REST API). ::: #### Populate with field selection `fields` and `populate` can be combined. #### GET /api/articles — Populate with field selection Combine fields and populate parameters to select specific fields on both the main entry and its relations. **URL:** ``` GET /api/articles?fields[0]=title&fields[1]=slug&populate[headerImage][fields][0]=name&populate[headerImage][fields][1]=url ``` **JavaScript:** ``` const qs = require('qs'); const query = qs.stringify( { fields: ['title', 'slug'], populate: { headerImage: { fields: ['name', 'url'], }, }, }, { encodeValuesOnly: true, // prettify URL } ); await request(\`/api/articles?\${query}\`); ``` **Response 200 OK:** ```json { "data": [ { "id": 1, "documentId": "h90lgohlzfpjf3bvan72mzll", "title": "Test Article", "slug": "test-article", "headerImage": { "id": 1, "documentId": "cf07g1dbusqr8mzmlbqvlegx", "name": "17520.jpg", "url": "/uploads/17520_73c601c014.jpg" } } ], "meta": {} } ``` #### Populate with filtering `filters` and `populate` can be combined. #### GET /api/articles — Populate with filtering Combine populate with sort and filter parameters to refine which related entries are returned. **URL:** ``` GET /api/articles?populate[categories][sort][0]=name%3Aasc&populate[categories][filters][name][$eq]=Cars ``` **JavaScript:** ``` const qs = require('qs'); const query = qs.stringify( { populate: { categories: { sort: ['name:asc'], filters: { name: { $eq: 'Cars', }, }, }, }, }, { encodeValuesOnly: true, // prettify URL } ); await request(\`/api/articles?\${query}\`); ``` **Response 200 OK:** ```json { "data": [ { "id": 1, "documentId": "a1b2c3d4e5d6f7g8h9i0jkl", "title": "Test Article", "categories": { "data": [ { "id": 2, "documentId": "jKd8djla9ndalk98hflj3", "name": "Cars" } ] } } ], "meta": {} } ``` :::note For many-to-many and other join-table relations, an explicit `sort` within a `populate` object overrides the default connect order. Omit `sort` to preserve the connect order (the order in which entries were associated). ::: :::tip Performance tip In production, always use explicit population instead of wildcards like `populate=*`. Limit population depth to 2-3 levels and consider centralizing population logic in route middlewares. See [Building High-Performance Strapi Applications](https://strapi.io/blog/building-high-performance-strapi-applications-common-pitfalls-and-best-practices) on the Strapi blog. ::: # Publication filter Source: https://docs.strapi.io/cms/api/rest/publication-filter # REST API: `publicationFilter` The [REST API](/cms/api/rest) accepts an optional `publicationFilter` query parameter when [Draft & Publish](/cms/features/draft-and-publish) is enabled. Use it to query derived publication cohorts such as never-published or modified documents. The [`status`](/cms/api/rest/status) parameter still selects whether each matching document returns its draft or published row. :::prerequisites The [Draft & Publish](/cms/features/draft-and-publish) feature must be enabled on the content-type. ::: ## Default `status` {#default-status} When `status` is omitted, the REST API defaults to `status=published` **before** applying `publicationFilter`. | Query | Effective behavior | | ----- | ------------------ | | `?publicationFilter=never-published` | Empty (cohort is draft-only; default status is `published`) | | `?status=draft&publicationFilter=never-published` | Never-published draft rows | | `?publicationFilter=modified` | Published rows in the modified cohort | | `?status=draft&publicationFilter=modified` | Draft rows in the modified cohort | | `?publicationFilter=published-without-draft` | Orphan published rows (default `status=published` is correct) | The Document Service API defaults to `status=draft` instead. See [Document Service API: default `status`](/cms/api/document-service/publication-filter#default-status). :::note Cohort definitions, the full `status` × `publicationFilter` matrix, Content Manager mapping, and validation rules are documented on [Document Service API: `publicationFilter`](/cms/api/document-service/publication-filter). ::: Accepted kebab-case values: `never-published`, `has-published-version`, `modified`, `unmodified`, `never-published-document`, `has-published-version-document`, `published-without-draft`, `published-with-draft`. Invalid values return HTTP `400`. ## Get never-published draft documents {#never-published} Pair-scoped `never-published` only matches draft rows. Pass `status=draft` because REST defaults to `status=published`. **Get draft restaurants that have never been published for their locale:** `GET /api/restaurants?status=draft&publicationFilter=never-published` **Example response:** ```json {6} { "data": [ { "documentId": "a1b2c3d4e5f6g7h8i9j0klm", "name": "New Restaurant", "publishedAt": null, "locale": "en" } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 1 } } } ``` ## Get modified documents {#modified} The `modified` cohort includes pairs where the draft row is newer than its published peer. With no `status` parameter, REST returns **published** rows from that cohort. Pass `status=draft` to return the draft rows instead. **Get published restaurants in the modified cohort (default status):** `GET /api/restaurants?publicationFilter=modified` **Example response:** ```json {6} { "data": [ { "documentId": "znrlzntu9ei5onjvwfaalu2v", "name": "Biscotte Restaurant", "publishedAt": "2024-03-14T15:40:45.330Z", "locale": "en" } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 1 } } } ``` ## Get published rows without a draft peer {#published-without-draft} The `published-without-draft` cohort matches published rows that have no draft sibling for the same `(documentId, locale)`. Because REST defaults to `status=published`, you can omit `status` in the query URL. **Get published restaurants with no draft row for the same locale:** `GET /api/restaurants?publicationFilter=published-without-draft` **Example response:** ```json {6} { "data": [ { "documentId": "abcdefghijklmno456", "name": "Legacy Restaurant", "publishedAt": "2024-01-10T09:15:00.000Z", "locale": "en" } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 1 } } } ``` ## Combine with other parameters {#combine} `publicationFilter` can be combined with [`filters`](/cms/api/rest/filters), [`locale`](/cms/api/rest/locale), [`populate`](/cms/api/rest/populate-select), and other [REST parameters](/cms/api/rest/parameters). All conditions are applied together. ## Deprecated `hasPublishedVersion` parameter {#has-published-version-deprecated} The boolean `hasPublishedVersion` query parameter is deprecated. Accepted values: `true`, `false`, `'true'`, or `'false'`. Strapi maps it to document-scoped `publicationFilter` values: | `hasPublishedVersion` | Maps to | | --------------------- | ------- | | `false` / `'false'` | `never-published-document` | | `true` / `'true'` | `has-published-version-document` | Example: `GET /api/restaurants?status=draft&hasPublishedVersion=false` If both `publicationFilter` and `hasPublishedVersion` are sent, `publicationFilter` takes precedence. Prefer `publicationFilter` for new integrations. # Relations Source: https://docs.strapi.io/cms/api/rest/relations # Managing relations with API requests Use `connect`, `disconnect`, and `set` parameters in REST and GraphQL API requests to manage relations between content-types. Reorder relations using positional arguments like `before`, `after`, `start`, or `end`. Defining relations between content-types (that are designated as entities in the database layers) is connecting entities with each other. Relations between content-types can be managed through the [admin panel](/cms/features/content-manager#relational-fields) or through [REST API](/cms/api/rest) or [Document Service API](/cms/api/document-service) requests. Relations can be connected, disconnected or set through the Content API by passing parameters in the body of the request. These payloads work for both single-entry relations and multi relations (one-to-many, many-to-one, many-to-many, and many-way). When a relational field allows multiple links, the API expects arrays of relation IDs and returns arrays in responses. | Parameter name | Description | Type of update | |-------------------------|-------------|----------------| | [`connect`](#connect) | Connects new entities.

Can be used in combination with `disconnect`.

Can be used with [positional arguments](#relations-reordering) to define an order for relations. | Partial | | [`disconnect`](#disconnect) | Disconnects entities.

Can be used in combination with `connect`. | Partial | | [`set`](#set) | Set entities to a specific set. Using `set` will overwrite all existing connections to other entities.

Cannot be used in combination with `connect` or `disconnect`. | Full | :::info Relations across REST, GraphQL, and Document Service The `connect`, `disconnect`, and `set` payloads described on this page apply to [REST](/cms/api/rest) and [GraphQL](/cms/api/graphql#fetch-relations) requests, and the same object shapes are supported when you call [Document Service](/cms/api/document-service) methods from server or plugin code. The Document Service introduction repeats this, and the Internationalization example further down on this page shows `strapi.documents(...).update()` with `connect`. If TypeScript reports `TS2353` and claims `connect` is not a valid property on your `data` object while you mirror these examples, treat that as a typings gap rather than Strapi rejecting the call at runtime. Until the Strapi type packages align fully, narrow the `data` payload with a type assertion or build it through a small helper typed more loosely so you can keep the documented shape. See [GitHub issue #2904](https://github.com/strapi/documentation/issues/2904) for discussion. ::: :::note When [Internationalization (i18n)](/cms/features/internationalization) is enabled on the content-type, you can also pass a locale to set relations for a specific locale, as in this Document Service API example: ```js await strapi.documents('api::restaurant.restaurant').update({ documentId: 'a1b2c3d4e5f6g7h8i9j0klm', locale: 'fr', data: { category: { connect: ['z0y2x4w6v8u1t3s5r7q9onm', 'j9k8l7m6n5o4p3q2r1s0tuv'] } } }) ``` :::tip TypeScript Workaround When using connect, disconnect, or set with the Document Service API in TypeScript, you may encounter a TS2353 error stating that these properties do not exist on type LongHandEntity. While the runtime engine fully supports this syntax, the current typings do not. You can safely bypass this validation by casting your data payload to any or a broader object type: ```js await strapi.documents('api::cart.cart').update({ documentId: cart.documentId, data: { status: 'checked_out', order: { connect: [order.documentId] } as any, // Temporary workaround }, }); ``` If no locale is passed, the default locale will be assumed. ::: ## `connect` Using `connect` in the body of a request performs a partial update, connecting the specified relations. `connect` accepts either a shorthand or a longhand syntax: | Syntax type | Syntax example | | ------------|----------------| | shorthand | `connect: ['z0y2x4w6v8u1t3s5r7q9onm', 'j9k8l7m6n5o4p3q2r1s0tuv']` | | longhand | ```connect: [{ documentId: 'z0y2x4w6v8u1t3s5r7q9onm' }, { documentId: 'j9k8l7m6n5o4p3q2r1s0tuv' }]``` | You can also use the longhand syntax to [reorder relations](#relations-reordering). `connect` can be used in combination with [`disconnect`](#disconnect). :::caution `connect` is not officially supported for media attributes. Advanced users can technically connect media entries by targeting upload file IDs, but this workaround isn't recommended or supported by Strapi and can easily break (e.g. when Draft & Publish uses mismatched IDs). Proceed with caution. ::: Sending the following request updates a `restaurant`, identified by its `documnentId` `a1b2c3d4e5f6g7h8i9j0klm`. The request uses the `categories` attribute to connect the restaurant with 2 categories identified by their `documentId`: `PUT` `http://localhost:1337/api/restaurants/a1b2c3d4e5f6g7h8i9j0klm` ```js { data: { categories: { connect: ['z0y2x4w6v8u1t3s5r7q9onm', 'j9k8l7m6n5o4p3q2r1s0tuv'] } } } ``` ```js const fetch = require('node-fetch'); const response = await fetch( 'http://localhost:1337/api/restaurants/a1b2c3d4e5f6g7h8i9j0klm', { method: 'put', body: { data: { categories: { connect: ['z0y2x4w6v8u1t3s5r7q9onm', 'j9k8l7m6n5o4p3q2r1s0tuv'] } } } } ); ``` Sending the following request updates a `restaurant`, identified by its `documnentId` `a1b2c3d4e5f6g7h8i9j0klm`. The request uses the `categories` attribute to connect the restaurant with 2 categories identified by their `documentId`: `PUT` `http://localhost:1337/api/restaurants/a1b2c3d4e5f6g7h8i9j0klm` ```js { data: { categories: { connect: [ { documentId: 'z0y2x4w6v8u1t3s5r7q9onm' }, { documentId: 'j9k8l7m6n5o4p3q2r1s0tuv' } ] } } } ``` ```js const fetch = require('node-fetch'); const response = await fetch( 'http://localhost:1337/api/restaurants/a1b2c3d4e5f6g7h8i9j0klm', { method: 'put', body: { data: { categories: { connect: [ { documentId: 'z0y2x4w6v8u1t3s5r7q9onm' }, { documentId: 'j9k8l7m6n5o4p3q2r1s0tuv' } ] } } } } ); ``` ### Relations reordering Positional arguments can be passed to the longhand syntax of `connect` to define the order of relations. The longhand syntax accepts an array of objects, each object containing the `documentId` of the entry to be connected and an optional `position` object to define where to connect the relation. :::note Different syntaxes for different relations The syntaxes described in this documentation are useful for one-to-many, many-to-many and many-ways relations.
For one-to-one, many-to-one and one-way relations, the syntaxes are also supported but only the last relation will be used, so it's preferable to use a shorter format (e.g.: `{ data: { category: 'a1b2c3d4e5f6g7h8i9j0klm' } }`, see [REST API documentation](/cms/api/rest#requests)). ::: To define the `position` for a relation, pass one of the following 4 different positional attributes: | Parameter name and syntax | Description | Type | | ------------------------- | ---------------------------------------------------------------------- | ---------- | | `before: documentId` | Positions the relation before the given `documentId`. | `documentId` (string) | | `after: documentId` | Positions the relation after the given `documentId`. | `documentId` (string) | | `start: true` | Positions the relation at the start of the existing list of relations. | Boolean | | `end: true` | Positions the relation at the end of the existing list of relations. | Boolean | The `position` argument is optional and defaults to `position: { end: true }`. :::note Sequential order Since `connect` is an array, the order of operations is important as they will be treated sequentially (see combined example below). ::: :::caution The same relation should not be connected more than once, otherwise it would return a Validation error by the API. ::: Consider the following record in the database: ```js categories: [ { documentId: 'j9k8l7m6n5o4p3q2r1s0tuv' } { documentId: 'z0y2x4w6v8u1t3s5r7q9onm' } ] ``` Sending the following request updates a `restaurant`, identified by its `documentId` `a1b2c3d4e5f6g7h8i9j0klm`, connecting a relation of entity with a `documentId` of `ma12bc34de56fg78hi90jkl` for the `categories` attribute and positioning it before the entity with `documentId` `z0y2x4w6v8u1t3s5r7q9onm`: `PUT http://localhost:1337/api/restaurants/a1b2c3d4e5f6g7h8i9j0klm` ```js { data: { categories: { connect: [ { documentId: 'ma12bc34de56fg78hi90jkl', position: { before: 'z0y2x4w6v8u1t3s5r7q9onm' } }, ] } } } ``` Consider the following record in the database: ```js categories: [ { documentId: 'j9k8l7m6n5o4p3q2r1s0tuv' } { documentId: 'z0y2x4w6v8u1t3s5r7q9onm' } ] ``` Sending the following example in the request body of a PUT request updates multiple relations: `PUT http://localhost:1337/api/restaurants/a1b2c3d4e5f6g7h8i9j0klm` ```js { data: { categories: { connect: [ { id: '6u86wkc6x3parjd4emikhmx', position: { after: 'j9k8l7m6n5o4p3q2r1s0tuv'} }, { id: '3r1wkvyjwv0b9b36s7hzpxl', position: { before: 'z0y2x4w6v8u1t3s5r7q9onm' } }, { id: 'rkyqa499i84197l29sbmwzl', position: { end: true } }, { id: 'srkvrr77k96o44d9v6ef1vu' }, { id: 'nyk7047azdgbtjqhl7btuxw', position: { start: true } }, ] } } } ``` Omitting the `position` argument (as in `documentId: 'srkvrr77k96o44d9v6ef1vu9'`) defaults to `position: { end: true }`. All other relations are positioned relative to another existing `id` (using `after` or `before`) or relative to the list of relations (using `start` or `end`). Operations are treated sequentially in the order defined in the `connect` array, so the resulting database record will be the following: ```js categories: [ { id: 'nyk7047azdgbtjqhl7btuxw' }, { id: 'j9k8l7m6n5o4p3q2r1s0tuv' }, { id: '6u86wkc6x3parjd4emikhmx6' }, { id: '3r1wkvyjwv0b9b36s7hzpxl7' }, { id: 'a1b2c3d4e5f6g7h8i9j0klm' }, { id: 'rkyqa499i84197l29sbmwzl' }, { id: 'srkvrr77k96o44d9v6ef1vu9' } ] ``` ### Edge cases: Draft & Publish or i18n disabled When some built-in features of Strapi 5 are disabled for a content-type, such as [Draft & Publish](/cms/features/draft-and-publish) and [Internationalization (i18)](/cms/features/internationalization), the `connect` parameter might be used differently: **Relation from a `Category` with i18n _off_ to an `Article` with i18n _on_:** In this situation you can select which locale you are connecting to: ```js data: { categories: { connect: [ { documentId: 'z0y2x4w6v8u1t3s5r7q9onm', locale: 'en' }, // Connect to the same document id but with a different locale 👇 { documentId: 'z0y2x4w6v8u1t3s5r7q9onm', locale: 'fr' }, ] } } ``` **Relation from a `Category` with Draft & Publish _off_ to an `Article` with Draft & Publish _on_:** ```js data: { categories: { connect: [ { documentId: 'z0y2x4w6v8u1t3s5r7q9onm', status: 'draft' }, // Connect to the same document id but with different publication states 👇 { documentId: 'z0y2x4w6v8u1t3s5r7q9onm', status: 'published' }, ] } } ``` ## `disconnect` Using `disconnect` in the body of a request performs a partial update, disconnecting the specified relations. `disconnect` accepts either a shorthand or a longhand syntax: | Syntax type | Syntax example | | ------------|----------------| | shorthand | `disconnect: ['z0y2x4w6v8u1t3s5r7q9onm', 'j9k8l7m6n5o4p3q2r1s0tuv']` | longhand | ```disconnect: [{ documentId: 'z0y2x4w6v8u1t3s5r7q9onm' }, { documentId: 'j9k8l7m6n5o4p3q2r1s0tuv' }]``` | `disconnect` can be used in combination with [`connect`](#connect).
Sending the following request updates a `restaurant`, identified by its `documentId` `a1b2c3d4e5f6g7h8i9j0klm`, disconnecting the relations with 2 entries identified by their `documentId`: `PUT http://localhost:1337/api/restaurants/a1b2c3d4e5f6g7h8i9j0klm` ```js { data: { categories: { disconnect: ['z0y2x4w6v8u1t3s5r7q9onm', 'j9k8l7m6n5o4p3q2r1s0tuv'], } } } ``` Sending the following request updates a `restaurant`, identified by its `documentId` `a1b2c3d4e5f6g7h8i9j0klm`, disconnecting the relations with 2 entries identified by their `documentId`: `PUT http://localhost:1337/api/restaurants/a1b2c3d4e5f6g7h8i9j0klm` ```js { data: { categories: { disconnect: [ { documentId: 'z0y2x4w6v8u1t3s5r7q9onm' }, { documentId: 'j9k8l7m6n5o4p3q2r1s0tuv' } ], } } } ``` ## `set` Using `set` performs a full update, replacing all existing relations with the ones specified, in the order specified. `set` accepts a shorthand or a longhand syntax: | Syntax type | Syntax example | | ----------- | ------------------------------- | | shorthand | `set: ['z0y2x4w6v8u1t3s5r7q9onm', 'j9k8l7m6n5o4p3q2r1s0tuv']` | | longhand | ```set: [{ documentId: 'z0y2x4w6v8u1t3s5r7q9onm' }, { documentId: 'j9k8l7m6n5o4p3q2r1s0tuv' }]``` | As `set` replaces all existing relations, it should not be used in combination with other parameters. To perform a partial update, use [`connect`](#connect) and [`disconnect`](#disconnect). :::note Omitting set Omitting any parameter is equivalent to using `set`.
For instance, the following 3 syntaxes are all equivalent: - `data: { categories: set: [{ documentId: 'z0y2x4w6v8u1t3s5r7q9onm' }, { documentId: 'j9k8l7m6n5o4p3q2r1s0tuv' }] }}` - `data: { categories: set: ['z0y2x4w6v8u1t3s5r7q9onm2', 'j9k8l7m6n5o4p3q2r1s0tuv'] }}` - `data: { categories: ['z0y2x4w6v8u1t3s5r7q9onm2', 'j9k8l7m6n5o4p3q2r1s0tuv'] }` ::: Sending the following request updates a `restaurant`, identified by its `documentId` `a1b2c3d4e5f6g7h8i9j0klm`, replacing all previously existing relations and using the `categories` attribute to connect 2 categories identified by their `documentId`: `PUT http://localhost:1337/api/restaurants/a1b2c3d4e5f6g7h8i9j0klm` ```js { data: { categories: { set: ['z0y2x4w6v8u1t3s5r7q9onm', 'j9k8l7m6n5o4p3q2r1s0tuv4'], } } } ``` Sending the following request updates a `restaurant`, identified by its `documentId` `a1b2c3d4e5f6g7h8i9j0klm`, replacing all previously existing relations and using the `categories` attribute to connect 2 categories identified by their `documentId`: `PUT http://localhost:1337/api/restaurants/a1b2c3d4e5f6g7h8i9j0klm` ```js { data: { categories: { set: [ { documentId: 'z0y2x4w6v8u1t3s5r7q9onm' }, { documentId: 'j9k8l7m6n5o4p3q2r1s0tuv' } ], } } } ``` # Sort and Pagination Source: https://docs.strapi.io/cms/api/rest/sort-pagination # REST API: Sort & Pagination Sort REST API results on one or multiple fields with `:asc` or `:desc` syntax, and paginate using either page-based or offset-based parameters. Entries that are returned by queries to the [REST API](/cms/api/rest) can be sorted and paginated. :::tip Strapi takes advantage of the ability of [the `qs` library](https://github.com/ljharb/qs) to parse nested objects to create more complex queries. Use `qs` directly to generate complex queries instead of creating them manually. Examples in this documentation showcase how you can use `qs`. You can also use the [interactive query builder](/cms/api/rest/interactive-query-builder) if you prefer playing with our online tool instead of generating queries with `qs` on your machine. ::: ## Sorting Queries can accept a `sort` parameter that allows sorting on one or multiple fields with the following syntaxes: - `GET /api/:pluralApiId?sort=value` to sort on 1 field - `GET /api/:pluralApiId?sort[0]=value1&sort[1]=value2` to sort on multiple fields (e.g. on 2 fields) The sorting order can be defined with: - `:asc` for ascending order (default order, can be omitted) - or `:desc` for descending order. ### Example: Sort using 2 fields You can sort by multiple fields by passing fields in a `sort` array. #### GET /api/restaurants — Sort using 2 fields Sort results by Description and Name fields. **REST:** ``` GET /api/restaurants?sort[0]=Description&sort[1]=Name ``` **JavaScript:** ``` const qs = require('qs'); const query = qs.stringify({ sort: ['Description', 'Name'], }, { encodeValuesOnly: true, // prettify URL }); await request(\`/api/restaurants?\${query}\`); ``` **Response 200 OK:** ```json { "data": [ { "id": 9, "documentId": "hgv1vny5cebq2l3czil1rpb3", "Name": "BMK Paris Bamako", "Description": [ { "type": "paragraph", "children": [ { "type": "text", "text": "A very short description goes here." } ] } ] // … }, { "id": 8, "documentId": "flzc8qrarj19ee0luix8knxn", "Name": "Restaurant D", "Description": [ { "type": "paragraph", "children": [ { "type": "text", "text": "A very short description goes here." } ] } ] // … } // … ], "meta": { // … } } ``` ### Example: Sort using 2 fields and set the order Using the `sort` parameter and defining `:asc` or `:desc` on sorted fields, you can get results sorted in a particular order. #### GET /api/restaurants — Sort using 2 fields and set the order Sort results by Description ascending and Name descending. **REST:** ``` GET /api/restaurants?sort[0]=Description:asc&sort[1]=Name:desc ``` **JavaScript:** ``` const qs = require('qs'); const query = qs.stringify({ sort: ['Description:asc', 'Name:desc'], }, { encodeValuesOnly: true, // prettify URL }); await request(\`/api/restaurants?\${query}\`); ``` **Response 200 OK:** ```json { "data": [ { "id": 8, "documentId": "flzc8qrarj19ee0luix8knxn", "Name": "Restaurant D", "Description": [ { "type": "paragraph", "children": [ { "type": "text", "text": "A very short description goes here." } ] } ] // … }, { "id": 9, "documentId": "hgv1vny5cebq2l3czil1rpb3", "Name": "BMK Paris Bamako", "Description": [ { "type": "paragraph", "children": [ { "type": "text", "text": "A very short description goes here." } ] } ] // … } // … ], "meta": { // … } } ``` ## Pagination Queries can accept `pagination` parameters. Results can be paginated: - either by [page](#pagination-by-page) (i.e., specifying a page number and the number of entries per page) - or by [offset](#pagination-by-offset) (i.e., specifying how many entries to skip and to return) :::note Pagination methods can not be mixed. Always use either `page` with `pageSize` **or** `start` with `limit`. ::: ### Pagination by page To paginate results by page, use the following parameters: | Parameter | Type | Description | Default | | ----------------------- | ------- | ------------------------------------------------------------------------- | ------- | | `pagination[page]` | Integer | Page number | 1 | | `pagination[pageSize]` | Integer | Page size | 25 | | `pagination[withCount]` | Boolean | Adds the total numbers of entries and the number of pages to the response | True | #### GET /api/articles — Pagination by page Return only 10 entries on page 1. **REST:** ``` GET /api/articles?pagination[page]=1&pagination[pageSize]=10 ``` **JavaScript:** ``` const qs = require('qs'); const query = qs.stringify({ pagination: { page: 1, pageSize: 10, }, }, { encodeValuesOnly: true, // prettify URL }); await request(\`/api/articles?\${query}\`); ``` **Response 200 OK:** ```json { "data": [ // ... ], "meta": { "pagination": { "page": 1, "pageSize": 10, "pageCount": 5, "total": 48 } } } ``` ### Pagination by offset To paginate results by offset, use the following parameters: | Parameter | Type | Description | Default | | ----------------------- | ------- | -------------------------------------------------------------- | ------- | | `pagination[start]` | Integer | Start value (i.e. first entry to return) | 0 | | `pagination[limit]` | Integer | Number of entries to return | 25 | | `pagination[withCount]` | Boolean | Toggles displaying the total number of entries to the response | `true` | :::tip The default and maximum values for `pagination[limit]` can be [configured in the `./config/api.js`](/cms/configurations/api) file with the `api.rest.defaultLimit` and `api.rest.maxLimit` keys. ::: #### GET /api/articles — Pagination by offset Return only the first 10 entries using offset. **REST:** ``` GET /api/articles?pagination[start]=0&pagination[limit]=10 ``` **JavaScript:** ``` const qs = require('qs'); const query = qs.stringify({ pagination: { start: 0, limit: 10, }, }, { encodeValuesOnly: true, // prettify URL }); await request(\`/api/articles?\${query}\`); ``` **Response 200 OK:** ```json { "data": [ // ... ], "meta": { "pagination": { "start": 0, "limit": 10, "total": 42 } } } ``` # Status Source: https://docs.strapi.io/cms/api/rest/status # REST API: `status` The REST API's `status` parameter filters documents by their publication state, returning either published versions (default) or drafts by passing `status=draft`. The [REST API](/cms/api/rest) offers the ability to filter results based on their status, draft or published. :::prerequisites The [Draft & Publish](/cms/features/draft-and-publish) feature should be enabled. ::: Queries can accept a `status` parameter to fetch documents based on their status: - `published`: returns only the published version of documents (default) - `draft`: returns only the draft version of documents :::tip In the response data, the `publishedAt` field is `null` for drafts. ::: :::note Since published versions are returned by default, passing no status parameter is equivalent to passing `status=published`. ::: For derived publication cohorts (never-published, modified, and others), see [REST API: `publicationFilter`](/cms/api/rest/publication-filter).

#### GET /api/articles?status=draft — Get draft versions of restaurants Returns draft versions of documents by passing the status=draft query parameter. **JavaScript:** ``` const qs = require('qs'); const query = qs.stringify({ status: 'draft', }, { encodeValuesOnly: true, // prettify URL }); await request(\`/api/articles?\${query}\`); ``` **Response 200 OK:** ```json { "data": [ { "id": 5, "documentId": "znrlzntu9ei5onjvwfaalu2v", "Name": "Biscotte Restaurant", "Description": [ { "type": "paragraph", "children": [ { "type": "text", "text": "This is the draft version." } ] } ], "createdAt": "2024-03-06T13:43:30.172Z", "updatedAt": "2024-03-06T21:38:46.353Z", "publishedAt": null, "locale": "en" } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 4 } } } ``` # Upload files Source: https://docs.strapi.io/cms/api/rest/upload # REST API: Upload files The `/api/upload` REST API endpoints enable you to upload files to the Media Library, retrieve paginated file lists, update file metadata, and delete files from your Strapi application. The [Media Library feature](/cms/features/media-library) is powered in the back-end server of Strapi by the `upload` package. To upload files to Strapi, you can either use the Media Library directly from the admin panel, or use the [REST API](/cms/api/rest), with the following available endpoints : | Method | Path | Description | | :----- | :------------------------- | :--------------------------- | | GET | `/api/upload/files` | Get a list of files | | GET | `/api/upload/files/page` | Get a paginated list of files | | GET | `/api/upload/files/:id` | Get a specific file | | POST | `/api/upload` | Upload files | | POST | `/api/upload?id=x` | Update fileInfo | | DELETE | `/api/upload/files/:id` | Delete a file | :::note Notes - [Folders](/cms/features/media-library#organizing-assets-with-folders) are an admin panel-only feature and are not part of the Content API (REST or GraphQL). Files uploaded through REST are located in the automatically created "API Uploads" folder. - The GraphQL API does not support uploading media files. To upload files, use the REST API or directly add files from the [Media Library](/cms/features/media-library) in the admin panel. Some GraphQL mutations to update or delete uploaded media files are still possible (see [GraphQL API documentation](/cms/api/graphql#mutations-on-media-files) for details). ::: ## Get a list of files 2 endpoints return files from the Media Library: `/api/upload/files` returns every file as a flat array, while `/api/upload/files/page` returns files using the standard paginated response. Use `/api/upload/files/page` for any non-trivial Media Library. ### Get all files `GET /api/upload/files` returns a flat array of all files in the Media Library: ```json [ { "id": 1, "documentId": "a1b2c3...", "name": "photo.jpg", "url": "/uploads/photo.jpg", "mime": "image/jpeg", "size": 12.34 // ...other file fields } // ... ] ``` This endpoint ignores pagination parameters and always returns every file. For large media libraries the response can be very large, so prefer `/api/upload/files/page` instead. ### Get a paginated list of files `GET /api/upload/files/page` returns files using the standard [paginated response](/cms/api/rest/sort-pagination), wrapped in a `data` array and a `meta.pagination` object. It accepts the same query parameters as other REST collection endpoints: | Parameter | Description | | --------- | ----------- | | `pagination[page]` | Page number (1-based). Default `1`. | | `pagination[pageSize]` | Number of files per page. Defaults to the `api.rest.defaultLimit` [configuration](/cms/configurations/api) (`25`), capped by `api.rest.maxLimit` if set. | | `pagination[start]` | Pagination by offset: number of files to skip. | | `pagination[limit]` | Pagination by offset: maximum number of files to return. | | `pagination[withCount]` | Whether to run the count query and include `total` and `pageCount` in the response. Defaults to `true`. | | `filters` | [Filter](/cms/api/rest/filters) the results. | | `sort` | [Sort](/cms/api/rest/sort-pagination#sorting) the results. | | `fields` | [Select](/cms/api/rest/populate-select#field-selection) which fields to return. | | `populate` | [Populate](/cms/api/rest/populate-select#population) relations. | :::note Pagination parameters use the nested format (`pagination[page]=2&pagination[pageSize]=10`), not the flat format (`page=2&pageSize=10`). Pagination by page (`page`/`pageSize`) and pagination by offset (`start`/`limit`) are mutually exclusive: combining them returns a `400` error. For details on both pagination methods, see [Sort & Pagination](/cms/api/rest/sort-pagination#pagination). ::: **Example request: Get the second page of 10 files:** `GET /api/upload/files/page?pagination[page]=2&pagination[pageSize]=10` **Example response:** ```json { "data": [ { "id": 1, "documentId": "a1b2c3...", "name": "photo.jpg", "url": "/uploads/photo.jpg", "mime": "image/jpeg", "size": 12.34 // ...other file fields } ], "meta": { "pagination": { "page": 2, "pageSize": 10, "pageCount": 4, "total": 100 } } } ``` When using pagination by offset, the `meta.pagination` object returns `start` and `limit` instead of `page` and `pageSize`: **Example request: Get 5 image files, skipping the first 20, without a total count:** `GET /api/upload/files/page?pagination[start]=20&pagination[limit]=5&pagination[withCount]=false&filters[mime][$startsWith]=image/` **Example response:** ```json { "data": [ // ... ], "meta": { "pagination": { "start": 20, "limit": 5 } } } ``` When `pagination[withCount]` is `false`, the count query is skipped and `total` and `pageCount` are omitted from the response. ## Upload files Upload one or more files to your application. `files` is the only accepted parameter, and describes the file(s) to upload. The value(s) can be a Buffer or Stream. :::info Signed URLs with private S3 buckets When using AWS S3 with the `ACL` parameter set to `"private"`, file URLs returned by the upload endpoints are automatically signed. Signed URLs include `X-Amz-Signature` query parameters and an `isUrlSigned: true` flag in the response, making them accessible despite the private bucket ACL. Signed URLs expire based on your `signedUrlExpires` configuration (default: 15 minutes). ::: :::tip When uploading an image, include a `fileInfo` object to set the file name, alt text, and caption. ::: ```html
``` ```js const file = await blobFrom('./1.png', 'image/png'); const form = new FormData(); form.append('files', file, "1.png"); form.append( 'fileInfo', JSON.stringify({ name: 'Homepage hero', alternativeText: 'Person smiling while holding laptop', caption: 'Hero image used on the homepage', }) ); const response = await fetch('http://localhost:1337/api/upload', { method: 'post', body: form, }); ``` :::caution You have to send FormData in your request body. ::: ## Upload entry files Upload one or more files that will be linked to a specific entry. The following parameters are accepted: | Parameter | Description | | --------- | ----------- | |`files` | The file(s) to upload. The value(s) can be a Buffer or Stream. | |`path` (optional) | The folder where the file(s) will be uploaded to (only supported on strapi-provider-upload-aws-s3). | | `refId` | The ID of the entry which the file(s) will be linked to. | | `ref` | The unique ID (uid) of the model which the file(s) will be linked to (see more below). | | `source` (optional) | The name of the plugin where the model is located. | | `field` | The field of the entry which the file(s) will be precisely linked to. | For example, given the `Restaurant` model attributes: ```json title="/src/api/restaurant/content-types/restaurant/schema.json" { // ... "attributes": { "name": { "type": "string" }, "cover": { "type": "media", "multiple": false, } } // ... } ``` The following is an example of a corresponding front-end code: ```html
``` :::caution You have to send FormData in your request body. ::: ## Update fileInfo Update a file in your application. `fileInfo` is the only accepted parameter, and describes the fileInfo to update: ```js const fileId = 50; const newFileData = { alternativeText: 'My new alternative text for this image!', }; const form = new FormData(); form.append('fileInfo', JSON.stringify(newFileData)); const response = await fetch(`http://localhost:1337/api/upload?id=${fileId}`, { method: 'post', body: form, }); ``` ## Models definition Adding a file attribute to a [model](/cms/backend-customization/models) (or the model of another plugin) is like adding a new association. The following example lets you upload and attach one file to the `avatar` attribute: ```json title="/src/api/restaurant/content-types/restaurant/schema.json" { // ... { "attributes": { "pseudo": { "type": "string", "required": true }, "email": { "type": "email", "required": true, "unique": true }, "avatar": { "type": "media", "multiple": false, } } } // ... } ``` The following example lets you upload and attach multiple pictures to the `restaurant` content-type: ```json title="/src/api/restaurant/content-types/restaurant/schema.json" { // ... { "attributes": { "name": { "type": "string", "required": true }, "covers": { "type": "media", "multiple": true, } } } // ... } ``` # Back-end customization Source: https://docs.strapi.io/cms/backend-customization
# Backend customization Strapi’s back end is a Koa-based server where requests pass through global middlewares, routes, controllers, services, and models before the Document Service returns responses. :::strapi Disambiguation: Strapi back end As a headless CMS, the Strapi software as a whole can be considered as the "back end" of your website or application. But the Strapi software itself includes 2 different parts: - The **back-end** part of Strapi is an HTTP server that Strapi runs. Like any HTTP server, the Strapi back end receives requests and send responses. Your content is stored in a database, and the Strapi back end interacts with the database to create, retrieve, update, and delete content. - The **front-end** part of Strapi is called the admin panel. The admin panel presents a graphical user interface to help you structure and manage the content. Throughout this developer documentation, 'back end' refers _exclusively_ to the back-end part of Strapi. The [Getting Started > Admin panel page](/cms/features/admin-panel) gives an admin panel overview and the [admin panel customization section](/cms/admin-panel-customization) details the various customization options available for the admin panel. ::: The Strapi back end runs an HTTP server based on [Koa](https://koajs.com/), a back-end JavaScript framework. Like any HTTP server, the Strapi back end receives requests and send responses. You can send requests to the Strapi back end to create, retrieve, update, or delete data through the [REST](/cms/api/rest) or [GraphQL](/cms/api/graphql) APIs. A request can travel through the Strapi back end as follows: 1. The Strapi server receives a [request](/cms/backend-customization/requests-responses). 2. The request hits [global middlewares](/cms/backend-customization/middlewares) that are run in a sequential order. 3. The request hits a [route](/cms/backend-customization/routes).
By default, Strapi generates route files for all the content-types that you create (see [REST API documentation](/cms/api/rest)), and more routes can be added and configured. 4. [Route policies](/cms/backend-customization/policies) act as a read-only validation step that can block access to a route. [Route middlewares](/cms/backend-customization/routes#middlewares) can control the request flow and mutate the request itself before moving forward. 5. [Controllers](/cms/backend-customization/controllers) execute code once a route has been reached. [Services](/cms/backend-customization/services) are optional, additional code that can be used to build custom logic reusable by controllers. 6. The code executed by the controllers and services interacts with the [models](/cms/backend-customization/models) that are a representation of the content content structure stored in the database.
Interacting with the data represented by the models is handled by the [Document Service](/cms/api/document-service) and [Query Engine](/cms/api/query-engine). 7. You can implement [Document Service middlewares](/cms/api/document-service/middlewares) to control the data before it's sent to the Query Engine. The Query Engine can also use lifecycle hooks though we recommend you use Document Service middlewares unless you absolutely need to directly interact with the database. 7. The server returns a [response](/cms/backend-customization/requests-responses). The response can travel back through route middlewares and global middlewares before being sent. Both global and route middlewares include an asynchronous callback function, `await next()`. Depending on what is returned by the middleware, the request will either go through a shorter or longer path through the back end: * If a middleware returns nothing, the request will continue travelling through the various core elements of the back end (i.e., controllers, services, and the other layers that interact with the database). * If a middleware returns before calling `await next()`, a response will be immediately sent, skipping the rest of the core elements. Then it will go back down the same chain it came up. :::info Please note that all customizations described in the pages of this section are only for the REST API. [GraphQL customizations](/cms/plugins/graphql#customization) are described in the GraphQL plugin documentation. ::: ## Interactive diagram The following diagram represents how requests travel through the Strapi back end. You can click on any shape to jump to the relevant page in the documentation.
# Controllers Source: https://docs.strapi.io/cms/backend-customization/controllers # Controllers Controllers bundle actions that handle business logic for each route within Strapi's MVC pattern. This documentation demonstrates generating controllers, extending core ones with `createCoreController`, and delegating heavy logic to services. Controllers are JavaScript files that contain a set of methods, called actions, reached by the client according to the requested [route](/cms/backend-customization/routes). Whenever a client requests the route, the action performs the business logic code and sends back the [response](/cms/backend-customization/requests-responses). Controllers represent the C in the model-view-controller (MVC) pattern. In most cases, the controllers will contain the bulk of a project's business logic. But as a controller's logic becomes more and more complicated, it's a good practice to use [services](/cms/backend-customization/services) to organize the code into re-usable parts.
Simplified Strapi backend diagram with controllers highlighted
The diagram represents a simplified version of how a request travels through the Strapi back end, with controllers highlighted. The backend customization introduction page includes a complete, interactive diagram.
:::caution Sanitize inputs and outputs When overriding core actions, always validate and sanitize queries and responses to avoid leaking private fields or bypassing access rules. Use `validateQuery` (optional), `sanitizeQuery` (recommended), and `sanitizeOutput` before returning data from custom actions. See the example below for a safe `find` override. ::: ## Implementation Controllers can be [generated or added manually](#adding-a-new-controller). Strapi provides a `createCoreController` factory function that automatically generates core controllers and allows building custom ones or [extend or replace the generated controllers](#extending-core-controllers). ### Adding a new controller A new controller can be implemented: - with the [interactive CLI command `strapi generate`](/cms/cli) - or manually by creating a JavaScript file: - in `./src/api/[api-name]/controllers/` for API controllers (this location matters as controllers are auto-loaded by Strapi from there) - or in a folder like `./src/plugins/[plugin-name]/server/controllers/` for plugin controllers, though they can be created elsewhere as long as the plugin interface is properly exported in the `strapi-server.js` file (see [Server API for Plugins documentation](/cms/plugins-development/server-api)) ```js title="./src/api/restaurant/controllers/restaurant.js" const { createCoreController } = require('@strapi/strapi').factories; module.exports = createCoreController('api::restaurant.restaurant', ({ strapi }) => ({ // Method 1: Creating an entirely custom action async exampleAction(ctx) { try { ctx.body = 'ok'; } catch (err) { ctx.body = err; } }, // Method 2: Wrapping a core action (leaves core logic in place) async find(ctx) { // some custom logic here ctx.query = { ...ctx.query, local: 'en' } // Calling the default core action const { data, meta } = await super.find(ctx); // some more custom logic meta.date = Date.now() return { data, meta }; }, // Method 3: Replacing a core action with proper sanitization async find(ctx) { // validateQuery (optional) // to throw an error on query params that are invalid or the user does not have access to await this.validateQuery(ctx); // sanitizeQuery to remove any query params that are invalid or the user does not have access to // It is strongly recommended to use sanitizeQuery even if validateQuery is used const sanitizedQueryParams = await this.sanitizeQuery(ctx); const { results, pagination } = await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams); const sanitizedResults = await this.sanitizeOutput(results, ctx); return this.transformResponse(sanitizedResults, { pagination }); } })); ``` ```ts title="./src/api/restaurant/controllers/restaurant.ts" // Method 1: Creating an entirely custom action async exampleAction(ctx) { try { ctx.body = 'ok'; } catch (err) { ctx.body = err; } }, // Method 2: Wrapping a core action (leaves core logic in place) async find(ctx) { // some custom logic here ctx.query = { ...ctx.query, local: 'en' } // Calling the default core action const { data, meta } = await super.find(ctx); // some more custom logic meta.date = Date.now() return { data, meta }; }, // Method 3: Replacing a core action with proper sanitization async find(ctx) { // validateQuery (optional) // to throw an error on query params that are invalid or the user does not have access to await this.validateQuery(ctx); // sanitizeQuery to remove any query params that are invalid or the user does not have access to // It is strongly recommended to use sanitizeQuery even if validateQuery is used const sanitizedQueryParams = await this.sanitizeQuery(ctx); const { results, pagination } = await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams); // sanitizeOutput to ensure the user does not receive any data they do not have access to const sanitizedResults = await this.sanitizeOutput(results, ctx); return this.transformResponse(sanitizedResults, { pagination }); } })); ``` Each controller action can be an `async` or `sync` function. Every action receives a context object (`ctx`) as a parameter. `ctx` contains the [request context](/cms/backend-customization/requests-responses#ctxrequest) and the [response context](/cms/backend-customization/requests-responses#ctxresponse).
Example: GET /hello route calling a basic controller A specific `GET /hello` [route](/cms/backend-customization/routes) is defined, the name of the router file (i.e. `index`) is used to call the controller handler (i.e. `index`). Every time a `GET /hello` request is sent to the server, Strapi calls the `index` action in the `hello.js` controller, which returns `Hello World!`: ```js "title="./src/api/hello/routes/hello.js" module.exports = { routes: [ { method: 'GET', path: '/hello', handler: 'api::hello.hello.index', } ] } ``` ```js title="./src/api/hello/controllers/hello.js" module.exports = { async index(ctx, next) { // called by GET /hello ctx.body = 'Hello World!'; // we could also send a JSON }, }; ``` ```js title="./src/api/hello/routes/hello.ts" routes: [ { method: 'GET', path: '/hello', handler: 'api::hello.hello.index', } ] } ``` ```js title="./src/api/hello/controllers/hello.ts" async index(ctx, next) { // called by GET /hello ctx.body = 'Hello World!'; // we could also send a JSON }, }; ```
:::note When a new [content-type](/cms/backend-customization/models#content-types) is created, Strapi builds a generic controller with placeholder code, ready to be customized. ::: :::tip To see a possible advanced usage for custom controllers, read the [services and controllers](/cms/backend-customization/examples/services-and-controllers) page of the backend customization examples cookbook. ::: ### Controllers & Routes: How routes reach controller actions - Core mapping is automatic: when you generate a content-type, Strapi creates the matching controller and a router file that already targets the standard actions (`find`, `findOne`, `create`, `update`, and `delete`). Overriding any of these actions inside the generated controller does not require touching the router - the route keeps the same handler string and executes your updated logic. - Adding a route should only be done for new actions or paths. If you introduce a brand-new method such as `exampleAction`, create or update a route entry whose `handler` points to the action so HTTP requests can reach it. Use the fully-qualified handler syntax `::..` (e.g. `api::restaurant.restaurant.exampleAction` for an API controller or `plugin::menus.menu.exampleAction` for a plugin controller). - Regarding controller and route filenames: the default controller name comes from the filename inside `./src/api/[api-name]/controllers/`. Core routers created with `createCoreRouter` adopt the same name, so the generated handler string matches automatically. Custom routers can follow any file naming scheme, as long as the `handler` string references an exported controller action. :::note About core mapping The REST routes Strapi generates for a content-type point each HTTP method at a handler string (for example `api::restaurant.restaurant.find`). That string already identifies the controller file and the exported action name (`find`, `findOne`, `create`, `update`, or `delete`). When you change the implementation inside those exports but keep the names, the router still calls your code. Edit the route file only when you add a new action name or path outside that default CRUD set. ::: The example below adds a new controller action and exposes it through a custom route without duplicating the existing CRUD route definitions: ```js title="./src/api/restaurant/controllers/restaurant.js" const { createCoreController } = require('@strapi/strapi').factories; module.exports = createCoreController('api::restaurant.restaurant', ({ strapi }) => ({ async exampleAction(ctx) { const specials = await strapi.service('api::restaurant.restaurant').find({ filters: { isSpecial: true } }); return this.transformResponse(specials.results); }, })); ``` ```js title="./src/api/restaurant/routes/01-custom-restaurant.js" module.exports = { routes: [ { method: 'GET', path: '/restaurants/specials', handler: 'api::restaurant.restaurant.exampleAction', }, ], }; ``` ### Sanitization and Validation in controllers {#sanitization-and-validation-in-controllers} :::warning It's strongly recommended you sanitize (v4.8.0+) and/or validate (v4.13.0+) your incoming request query utilizing the new `sanitizeQuery` and `validateQuery` functions to prevent the leaking of private data. ::: Sanitization means that the object is “cleaned” and returned. Validation means an assertion is made that the data is already clean and throws an error if something is found that shouldn't be there. In Strapi 5, both query parameters and input data (i.e., create and update body data) are validated. Any create and update data requests with the following invalid input will throw a `400 Bad Request` error: - relations the user do not have permission to create - unrecognized values that are not present on a schema - non-writable fields and internal timestamps like `createdAt` and `createdBy` fields - setting or updating an `id` field (except for connecting relations) :::note Internal `entity-validator` service Strapi exposes a low-level `entity-validator` service (accessible through `strapi.service('entity-validator')`) with `validateEntityCreation` and `validateEntityUpdate` methods that the document service uses internally. These are not part of the public API and their semantics can change between releases. From a custom controller, service, or middleware, use the documented `validateInput` factory helper or the [`strapi.contentAPI.validate.*` helpers](#sanitize-validate-custom-controllers) instead. They apply the same content-type schema rules and additionally account for permissions, non-writable fields, and the request authentication strategy. ::: #### Sanitization when utilizing controller factories Within the Strapi factories the following functions are exposed that can be used for sanitization and validation: | Function Name | Parameters | Description | |------------------|----------------------------|--------------------------------------------------------------------------------------| | `sanitizeQuery` | `ctx` | Sanitizes the request query | | `sanitizeOutput` | `entity`/`entities`, `ctx` | Sanitizes the output data where entity/entities should be an object or array of data | | `sanitizeInput` | `data`, `ctx` | Sanitizes the input data | | `validateQuery` | `ctx` | Validates the request query (throws an error on invalid params) | | `validateInput` | `data`, `ctx` | (EXPERIMENTAL) Validates the input data (throws an error on invalid data) | These functions automatically inherit the sanitization settings from the model and sanitize the data accordingly based on the content-type schema and any of the content API authentication strategies, such as the Users & Permissions plugin or API tokens. :::warning Because these methods use the model associated with the current controller, if you query data that is from another model (i.e., doing a find for "menus" within a "restaurant" controller method), you must instead use the `strapi.contentAPI` methods, such as `strapi.contentAPI.sanitize.query` described in [Sanitizing Custom Controllers](#sanitize-validate-custom-controllers), or else the result of your query will be sanitized against the wrong model. ::: ```js title="./src/api/restaurant/controllers/restaurant.js" const { createCoreController } = require('@strapi/strapi').factories; module.exports = createCoreController('api::restaurant.restaurant', ({ strapi }) => ({ async find(ctx) { await this.validateQuery(ctx); const sanitizedQueryParams = await this.sanitizeQuery(ctx); const { results, pagination } = await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams); const sanitizedResults = await this.sanitizeOutput(results, ctx); return this.transformResponse(sanitizedResults, { pagination }); } })); ``` ```js title="./src/api/restaurant/controllers/restaurant.ts" async find(ctx) { const sanitizedQueryParams = await this.sanitizeQuery(ctx); const { results, pagination } = await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams); const sanitizedResults = await this.sanitizeOutput(results, ctx); return this.transformResponse(sanitizedResults, { pagination }); } })); ``` #### Sanitization and validation when building custom controllers {#sanitize-validate-custom-controllers} Within custom controllers, Strapi exposes the following functions via `strapi.contentAPI` for sanitization and validation. To add custom query or body parameters to Content API routes (e.g. in `register`), see [Custom Content API parameters](/cms/backend-customization/routes#custom-content-api-parameters). | Function Name | Parameters | Description | |------------------------------|--------------------|---------------------------------------------------------| | `strapi.contentAPI.sanitize.input` | `data`, `schema`, `auth` | Sanitizes the request input including non-writable fields, removing restricted relations, and other nested "visitors" added by plugins | | `strapi.contentAPI.sanitize.output` | `data`, `schema`, `auth` | Sanitizes the response output including restricted relations, private fields, passwords, and other nested "visitors" added by plugins | | `strapi.contentAPI.sanitize.query` | `ctx.query`, `schema`, `auth` | Sanitizes the request query including filters, sort, fields, and populate | | `strapi.contentAPI.validate.query` | `ctx.query`, `schema`, `auth` | Validates the request query including filters, sort, fields (currently not populate) | | `strapi.contentAPI.validate.input` | `data`, `schema`, `auth` | (EXPERIMENTAL) Validates the request input including non-writable fields, removing restricted relations, and other nested "visitors" added by plugins | :::note Depending on the complexity of your custom controllers, you may need additional sanitization that Strapi cannot currently account for, especially when combining the data from multiple sources. ::: ```js title="./src/api/restaurant/controllers/restaurant.js" module.exports = { async findCustom(ctx) { const contentType = strapi.contentType('api::test.test'); await strapi.contentAPI.validate.query(ctx.query, contentType, { auth: ctx.state.auth }); const sanitizedQueryParams = await strapi.contentAPI.sanitize.query(ctx.query, contentType, { auth: ctx.state.auth }); const documents = await strapi.documents(contentType.uid).findMany(sanitizedQueryParams); return await strapi.contentAPI.sanitize.output(documents, contentType, { auth: ctx.state.auth }); } } ``` ```js title="./src/api/restaurant/controllers/restaurant.ts" async findCustom(ctx) { const contentType = strapi.contentType('api::test.test'); await strapi.contentAPI.validate.query(ctx.query, contentType, { auth: ctx.state.auth }); const sanitizedQueryParams = await strapi.contentAPI.sanitize.query(ctx.query, contentType, { auth: ctx.state.auth }); const documents = await strapi.documents(contentType.uid).findMany(sanitizedQueryParams); return await strapi.contentAPI.sanitize.output(documents, contentType, { auth: ctx.state.auth }); } } ``` ### Extending core controllers {#extending-core-controllers} Default controllers and actions are created for each content-type. These default controllers are used to return responses to API requests (e.g. when `GET /api/articles/3` is accessed, the `findOne` action of the default controller for the "Article" content-type is called). Default controllers can be customized to implement your own logic. The following code examples should help you get started. :::tip - An action from a core controller can be replaced entirely by [creating a custom action](#adding-a-new-controller) and naming the action the same as the original action (e.g. `find`, `findOne`, `create`, `update`, or `delete`). - When extending a core controller, you do not need to re-implement any sanitization as it will already be handled by the core controller you are extending. Where possible it's strongly recommended to extend the core controller instead of creating a custom controller. :::
Collection type examples :::tip The [backend customization examples cookbook](/cms/backend-customization/examples) shows how you can overwrite a default controller action, for instance for the [`create` action](/cms/backend-customization/examples/services-and-controllers#custom-controller). ::: ```js async find(ctx) { // some logic here const { data, meta } = await super.find(ctx); // some more logic return { data, meta }; } ``` ```js async findOne(ctx) { // some logic here const response = await super.findOne(ctx); // some more logic return response; } ``` ```js async create(ctx) { // some logic here const response = await super.create(ctx); // some more logic return response; } ``` ```js async update(ctx) { // some logic here const response = await super.update(ctx); // some more logic return response; } ``` ```js async delete(ctx) { // some logic here const response = await super.delete(ctx); // some more logic return response; } ```
Single type examples ```js async find(ctx) { // some logic here const response = await super.find(ctx); // some more logic return response; } ``` ```js async update(ctx) { // some logic here const response = await super.update(ctx); // some more logic return response; } ``` ```js async delete(ctx) { // some logic here const response = await super.delete(ctx); // some more logic return response; } ```
## Usage Controllers are declared and attached to a route. Controllers are automatically called when the route is called, so controllers usually do not need to be called explicitly. However, [services](/cms/backend-customization/services) can call controllers, and in this case the following syntax should be used: ```js // access an API controller strapi.controller('api::api-name.controller-name'); // access a plugin controller strapi.controller('plugin::plugin-name.controller-name'); ``` :::tip To list all the available controllers, run `yarn strapi controllers:list`. ::: # Backend Customization Examples Cookbook Source: https://docs.strapi.io/cms/backend-customization/examples # Backend customization: An examples cookbook using FoodAdvisor A cookbook of real-world backend customization examples using the FoodAdvisor demo application, demonstrating how to implement custom routes, controllers, services, policies, and middlewares in Strapi. :::callout 🏗 About these examples These examples are built around [FoodAdvisor](https://github.com/strapi/foodadvisor), which is no longer Strapi's featured demo application (it has been superseded by [LaunchPad](https://github.com/strapi/LaunchPad)). What matters here is understanding the backend mechanisms being demonstrated, not FoodAdvisor itself. These pages will be revisited to use LaunchPad. ::: The present section of the documentation is intended for developers who would like to get a deeper understanding of the Strapi back end customization possibilities. The section is a collection of examples that demonstrate how the core components of the back-end server of Strapi can be used in a real-world project. Front-end code that interacts with the back end may also be part of some examples, but displayed in collapsed blocks by default since front-end code examples are not the main focus of this cookbook. Examples are meant to extend the features of [FoodAdvisor](https://github.com/strapi/foodadvisor), the official Strapi demo application. FoodAdvisor builds a ready-made restaurants directory powered by a Strapi back end (included in the `/api` folder) and renders a [Next.js](https://nextjs.org/)-powered front-end website (included in the `/client` folder). :::prerequisites - 👀 You have read the [Quick Start Guide](/cms/quick-start) and/or understood that Strapi is a **headless CMS** A headless CMS is a Content Management System that separates the presentation layer (i.e., the front end, where content is displayed) from the back end (where content is managed).

Strapi is a headless CMS that provides:
  • a back-end server exposing an API for your content,
  • and a graphical user interface, called the admin panel, to manage the content.
The presentation layer should be handled by another framework, not by Strapi. that helps you create a content structure with the [Content-Type Builder](/cms/features/content-type-builder) and add some content through the [Content Manager](/cms/features/content-manager), then exposes the content through APIs. - 👀 You have read the [back-end customization introduction](/cms/backend-customization) to get a general understanding of what routes, policies, middlewares, controllers, and services are in Strapi. - 👷 If you want to test and play with the code examples by yourself, ensure you have cloned the [FoodAdvisor](https://github.com/strapi/foodadvisor) repository, setup the project, and started both the front-end and back-end servers. The Strapi admin panel should be accessible from [`localhost:1337/admin`](http://localhost:1337/admin) and the Next.js-based FoodAdvisor front-end website should be running on [`localhost:3000`](http://localhost:3000). ::: This section can be read from start to finish, or you might want to jump directly to a specific page to understand how a given core element from the Strapi back end can be used to solve a real-world use case example: | I want to understand… | Dedicated page | |------------|---------------| | How to authenticate my queries | [Authentication flow with JWT](/cms/backend-customization/examples/authentication) | | How and when to use
custom controllers and services | [Custom controllers and services examples](/cms/backend-customization/examples/services-and-controllers) | | How to use custom policies
and send custom errors | [Custom policies examples](/cms/backend-customization/examples/policies) | | How to configure and use custom routes | [Custom routes examples](/cms/backend-customization/examples/routes) | | How and when to use
custom global middlewares | [Custom middleware example](/cms/backend-customization/examples/middlewares) | # Authentication flow with JWT Source: https://docs.strapi.io/cms/backend-customization/examples/authentication # Examples cookbook: Authentication flow with JWT Authenticate REST API requests using JWT by sending credentials to the `/auth/local` endpoint and storing the token in `localStorage`, with optional session management for refresh token support. :::prerequisites This page is part of the back end customization examples cookbook. Please ensure you've read its [introduction](/cms/backend-customization/examples). ::: **💭 Context:** Out of the box, the front-end website of [FoodAdvisor](https://github.com/strapi/foodadvisor) does not provide any log in functionality. Logging in is done by accessing Strapi's admin panel at [`localhost:1337/admin`](http://localhost:1337/admin`). Let's add a basic login page to the front-end, [Next.js](https://nextjs.org/)-powered website included in the `/client` folder of FoodAdvisor. The login page will be accessible at [`localhost:3000/auth/login`](http://localhost:3000/auth/login) and contain a typical email/password login form. This will allow programmatically authenticating API requests sent to Strapi.
Example login page
A possible example of a login form on the front-end website of FoodAdvisor
**🎯 Goal**: Create a front-end component to: 1. to display a login form, 2. send a request to the `/auth/local` route of the Strapi back-end server to authenticate, 3. get a [JSON Web Token](https://en.wikipedia.org/wiki/JSON_Web_Token) (JWT), 4. and store the JWT into the [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) property of your browser for later retrieval and authentication of our requests. **Related concept** Additional information about JWT authentication can be found in the [Users & Permissions plugin](/cms/features/users-permissions) documentation. **🧑‍💻 Code example:** To achieve this, in the `/client` folder of the [FoodAdvisor](https://github.com/strapi/foodadvisor) project, you could create a `pages/auth/login.js` file that contains the following example code. Highlighted lines show the request sent to the `/auth/local` route provided by Strapi's Users & Permissions plugin: This file uses the formik package - install it using `yarn add formik` and restart the dev server. ```jsx title="/client/pages/auth/login.js" {21-27} const Login = () => { const { handleSubmit, handleChange } = useFormik({ initialValues: { identifier: '', password: '', }, onSubmit: async (values) => { /** * API URLs in Strapi are by default prefixed with /api, * but because the API prefix can be configured * with the rest.prefix property in the config/api.js file, * we use the getStrapiURL() method to build the proper full auth URL. **/ const res = await fetch(getStrapiURL('/auth/local'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(values), }); /** * Gets the JWT from the server response. * The actual response is { jwt, user }, but we only need the JWT here. */ const { jwt } = await res.json(); /** * Stores the JWT in the localStorage of the browser. * A better implementation would be to do this with an authentication context provider * or something more sophisticated, but it's not the purpose of this tutorial. */ localStorage.setItem('token', jwt); }, }); /** * The following code renders a basic login form * accessible from the localhost:3000/auth/login page. */ return (

Login

Login
); }; ``` ## Enhanced authentication with session management The above example uses the traditional JWT approach. For enhanced security, you can enable session management mode in your Users & Permissions configuration, which provides shorter-lived access tokens and refresh token functionality. ### Configuration First, enable session management in your `/config/plugins.js`: ```js title="/config/plugins.js" module.exports = ({ env }) => ({ 'users-permissions': { config: { jwtManagement: 'refresh', sessions: { accessTokenLifespan: 600, // 10 minutes (default) maxRefreshTokenLifespan: 2592000, // 30 days (default) idleRefreshTokenLifespan: 1209600, // 14 days (default) maxSessionLifespan: 86400, // 1 day (default) idleSessionLifespan: 7200, // 2 hours (default) }, }, }, }); ``` ### Enhanced login component Here's an updated login component that handles both JWT and refresh tokens: ```jsx title="/client/pages/auth/enhanced-login.js" const EnhancedLogin = () => { const [isLoading, setIsLoading] = useState(false); const { handleSubmit, handleChange } = useFormik({ initialValues: { identifier: '', password: '', }, onSubmit: async (values) => { setIsLoading(true); try { const res = await fetch(getStrapiURL('/auth/local'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(values), }); const data = await res.json(); if (res.ok) { // Store both tokens (session management mode) if (data.refreshToken) { localStorage.setItem('accessToken', data.jwt); localStorage.setItem('refreshToken', data.refreshToken); } else { // Legacy mode - single JWT localStorage.setItem('token', data.jwt); } // Redirect to protected area window.location.href = '/dashboard'; } else { console.error('Login failed:', data.error); } } catch (error) { console.error('Login error:', error); } finally { setIsLoading(false); } }, }); return (

Enhanced Login

{isLoading ? 'Logging in...' : 'Login'}
); }; ```
:::strapi What's next? Learn more about how custom [services and controllers](/cms/backend-customization/examples/services-and-controllers) can help you tweak a Strapi-based application. ::: # Custom middlewares Source: https://docs.strapi.io/cms/backend-customization/examples/middlewares # Examples cookbook: Custom global middlewares Custom global middlewares intercept incoming requests before controller execution, enabling you to add logic like analytics tracking. This example creates a middleware that logs restaurant page visits to a Google Sheet. :::callout 🏗 About these examples These examples are built around [FoodAdvisor](https://github.com/strapi/foodadvisor), which is no longer Strapi's featured demo application (it has been superseded by [LaunchPad](https://github.com/strapi/LaunchPad)). What matters here is understanding the backend mechanisms being demonstrated, not FoodAdvisor itself. These pages will be revisited to use LaunchPad. ::: :::prerequisites This page is part of the back end customization examples cookbook. Please ensure you've read its [introduction](/cms/backend-customization/examples). ::: Out of the box, [FoodAdvisor](https://github.com/strapi/foodadvisor) does not provide any custom middlewares that could use incoming requests and perform some additional logic before executing the controller code. There are 2 types of middlewares in Strapi: **route middlewares** control access to a route while **global middlewares** have a wider scope (see reference documentation for [middlewares customization](/cms/backend-customization/middlewares)). Custom route middlewares could be used instead of policies to control access to an endpoint (see [policies cookbook](/cms/backend-customization/examples/policies)) and could modify the context before passing it down to further core elements of the Strapi server. This page will _not_ cover custom route middlewares but rather illustrate a more elaborated usage for **custom global middlewares**. ## Populating an analytics dashboard in Google Sheets with a custom middleware **💭 Context:** In essence, a middleware gets executed between a request arriving at the server and the controller function getting executed. So, for instance, a middleware is a good place to perform some analytics. Let’s create a rudimentary example of an analytics dashboard made with Google Spreadsheets to have some insights on which restaurants pages of [FoodAdvisor](https://github.com/strapi/foodadvisor) are more visited.
Visiting a restaurant page updates the Google Sheets spreadsheet
Every GET request to a restaurant's page executes the code of a custom middleware, updating a Google Sheets spreadsheet in real-time.
**🎯 Goals**: - Create some utility functions that interact with Google Sheets. - Create a custom Strapi middleware that will create and/or update an existing Google Sheet document every time we have an incoming request to a Restaurants page of the FoodAdvisor project. - Append the custom middleware to the route where we want it to get executed. **Related concept** Additional information can be found in the [middlewares customization](/cms/backend-customization/middlewares) documentation. **🧑‍💻 Code example:** 1. In the `/api` folder of the [FoodAdvisor](https://github.com/strapi/foodadvisor) project, create a `/restaurant/middlewares/utils.js` file with the following example code:
Example utility functions that could be used to read, write and update a Google spreadsheet: The following code allows reading, writing, and updating a Google spreadsheet given an API Key read from a JSON file and a spreadsheet ID retrieved from the URL: ![Google Spreadsheet URL](/img/assets/backend-customization/tutorial-spreadsheet-url.png) Additional information can be found in the official [Google Sheets API documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values?hl=es-419). ```jsx title="src/api/restaurant/middlewares/utils.js" const { google } = require('googleapis'); const createGoogleSheetClient = async ({ keyFile, sheetId, tabName, range, }) => { async function getGoogleSheetClient() { const auth = new google.auth.GoogleAuth({ keyFile, scopes: ['https://www.googleapis.com/auth/spreadsheets'], }); const authClient = await auth.getClient(); return google.sheets({ version: 'v4', auth: authClient, }); } const googleSheetClient = await getGoogleSheetClient(); const writeGoogleSheet = async (data) => { googleSheetClient.spreadsheets.values.append({ spreadsheetId: sheetId, range: `${tabName}!${range}`, valueInputOption: 'USER_ENTERED', insertDataOption: 'INSERT_ROWS', resource: { majorDimension: 'ROWS', values: data, }, }); }; const updateoogleSheet = async (cell, data) => { googleSheetClient.spreadsheets.values.update({ spreadsheetId: sheetId, range: `${tabName}!${cell}`, valueInputOption: 'USER_ENTERED', resource: { majorDimension: 'ROWS', values: data, }, }); }; const readGoogleSheet = async () => { const res = await googleSheetClient.spreadsheets.values.get({ spreadsheetId: sheetId, range: `${tabName}!${range}`, }); return res.data.values; }; return { writeGoogleSheet, updateoogleSheet, readGoogleSheet, }; }; module.exports = { createGoogleSheetClient, }; ```
2. In the `/api` folder of the FoodAdvisor project, create a custom `analytics` middleware with the following code: ```jsx title="src/api/restaurant/middlewares/analytics.js" 'use strict'; const { createGoogleSheetClient } = require('./utils'); const serviceAccountKeyFile = './gs-keys.json'; // Replace the sheetId value with the corresponding id found in your own URL const sheetId = '1P7Oeh84c18NlHp1Zy-5kXD8zgpoA1WmvYL62T4GWpfk'; const tabName = 'Restaurants'; const range = 'A2:C'; const VIEWS_CELL = 'C'; const transformGSheetToObject = (response) => response.reduce( (acc, restaurant) => ({ ...acc, [restaurant[0]]: { id: restaurant[0], name: restaurant[1], views: restaurant[2], cellNum: Object.keys(acc).length + 2 // + 2 because we need to consider the header and that the initial length is 0, so our first real row would be 2, }, }), {} ); module.exports = (config, { strapi }) => { return async (context, next) => { // Generating google sheet client const { readGoogleSheet, updateoogleSheet, writeGoogleSheet } = await createGoogleSheetClient({ keyFile: serviceAccountKeyFile, range, sheetId, tabName, }); // Get the restaurant documentId from the params in the URL const restaurantId = context.params.id; const restaurant = await strapi.documents('api::restaurant.restaurant').findOne({ documentId: restaurantId, }); // Read the spreadsheet to get the current data const restaurantAnalytics = await readGoogleSheet(); /** * The returned data comes in the shape [1, "Mint Lounge", 23], * and we need to transform it into an object: {id: 1, name: "Mint Lounge", views: 23, cellNum: 2} */ const requestedRestaurant = transformGSheetToObject(restaurantAnalytics)[restaurantId]; if (requestedRestaurant) { await updateoogleSheet( `${VIEWS_CELL}${requestedRestaurant.cellNum}:${VIEWS_CELL}${requestedRestaurant.cellNum}`, [[Number(requestedRestaurant.views) + 1]] ); } else { /** If we don't have the restaurant in the spreadsheet already, * we create it with 1 view. */ const newRestaurant = [[restaurant.id, restaurant.name, 1]]; await writeGoogleSheet(newRestaurant); } // Call next to continue with the flow and get to the controller await next(); }; }; ``` 3. Configure the routes for the "Restaurants" content-type to execute the custom `analytics` middleware whenever a restaurant page is queried. To do so, use the following code: ```jsx title="src/api/restaurant/routes/restaurant.js" 'use strict'; const { createCoreRouter } = require('@strapi/strapi').factories; module.exports = createCoreRouter('api::restaurant.restaurant', { config: { findOne: { auth: false, policies: [], middlewares: ['api::restaurant.analytics'], }, }, }); ``` # Custom policies Source: https://docs.strapi.io/cms/backend-customization/examples/policies # Examples cookbook: Custom policies Custom policies control access to content-type endpoints by allowing or blocking requests, and can throw custom errors using `PolicyError` for better error handling and front-end integration. :::callout 🏗 About these examples These examples are built around [FoodAdvisor](https://github.com/strapi/foodadvisor), which is no longer Strapi's featured demo application (it has been superseded by [LaunchPad](https://github.com/strapi/LaunchPad)). What matters here is understanding the backend mechanisms being demonstrated, not FoodAdvisor itself. These pages will be revisited to use LaunchPad. ::: :::prerequisites This page is part of the back end customization examples cookbook. Please ensure you've read its [introduction](/cms/backend-customization/examples). ::: Out of the box, [FoodAdvisor](https://github.com/strapi/foodadvisor) does not use any custom policies or route middlewares that could control access to content type endpoints. In Strapi, controlling access to a content-type endpoint can be done either with a policy or route middleware: - policies are read-only and allow a request to pass or return an error, - while route middlewares can perform additional logic. In our example, let's use a policy. ## Creating a custom policy **💭 Context:** Let's say we would like to customize the backend of [FoodAdvisor](https://github.com/strapi/foodadvisor) to prevent restaurant owners from creating fake reviews for their businesses using a [form previously created](/cms/backend-customization/examples/services-and-controllers#rest-api-queries-from-the-front-end) on the front-end website. **🎯 Goals**: 1. Create a new folder for policies to apply only to the "Reviews" collection type. 2. Create a new policy file. 3. Use the `findMany()` method from the Document Service API to get information about the owner of a restaurant when the `/reviews` endpoint is reached. 4. Return an error if the authenticated user is the restaurant's owner, or let the request pass in other cases. **Related concepts** Additional information can be found in the [Policies](/cms/backend-customization/policies), [Routes](/cms/backend-customization/routes), and [Document Service API](/cms/api/document-service) documentation. **🧑‍💻 Code example:** In the `/api` folder of the [FoodAdvisor](https://github.com/strapi/foodadvisor) project, create a new `src/api/review/policies/is-owner-review.js` file with the following code: ```jsx title="src/api/review/policies/is-owner-review.js" module.exports = async (policyContext, config, { strapi }) => { const { body } = policyContext.request; const { user } = policyContext.state; // Return an error if there is no authenticated user with the request if (!user) { return false; } /** * Queries the Restaurants collection type * using the Document Service API * to retrieve information about the restaurant's owner. */ const [restaurant] = await strapi.documents('api::restaurant.restaurant').findMany({ filters: { slug: body.restaurant, }, populate: ['owner'], }); if (!restaurant) { return false; } /** * If the user submitting the request is the restaurant's owner, * we don't allow the review creation. */ if (user.id === restaurant.owner.id) { return false; } return true; }; ``` :::caution Policies or route middlewares should be declared in the configuration of a route to actually control access. Read more about routes in the [reference documentation](/cms/backend-customization/routes) or see an example in the [routes cookbook](/cms/backend-customization/examples/routes). ::: ## Sending custom errors through policies **💭 Context:** Out of the box, [FoodAdvisor](https://github.com/strapi/foodadvisor) sends a default error when a policy refuses access to a route. Let's say we want to customize the error sent when the [previously created custom policy](#creating-a-custom-policy) does not allow creating a review. **🎯 Goal:** Configure the custom policy to throw a custom error instead of the default error. **Related concept** Additional information can be found in the [Error handling](/cms/error-handling) documentation. **🧑‍💻 Code example:** In the `/api` folder of the [FoodAdvisor](https://github.com/strapi/foodadvisor) project, update the [previously created `is-owner-review` custom policy](#creating-a-custom-policy) as follows (highlighted lines are the only modified lines): ```jsx title="src/api/review/policies/is-owner-review.js" showLineNumbers const { errors } = require('@strapi/utils'); const { PolicyError } = errors; module.exports = async (policyContext, config, { strapi }) => { const { body } = policyContext.request; const { user } = policyContext.state; // Return an error if there is no authenticated user with the request if (!user) { return false; } /** * Queries the Restaurants collection type * using the Document Service API * to retrieve information about the restaurant's owner. */ const filteredRestaurants = await strapi.documents('api::restaurant.restaurant').findMany({ filters: { slug: body.restaurant, }, populate: ['owner'], }); const restaurant = filteredRestaurants[0]; if (!restaurant) { return false; } /** * If the user submitting the request is the restaurant's owner, * we don't allow the review creation. */ if (user.id === restaurant.owner.id) { // highlight-start /** * Throws a custom policy error * instead of just returning false * (which would result into a generic Policy Error). */ throw new PolicyError('The owner of the restaurant cannot submit reviews', { errCode: 'RESTAURANT_OWNER_REVIEW', // can be useful for identifying different errors on the front end }); // highlight-end } return true; }; ```
Responses sent with default policy error vs. custom policy error: When a policy refuses access to a route and a default error is thrown, the following response will be sent when trying to query the content-type through the REST API: ```jsx { "data": null, "error": { "status": 403, "name": "PolicyError", "message": "Policy Failed", "details": {} } } ``` When a policy refuses access to a route and the custom policy throws the custom error defined in the code example above, the following response will be sent when trying to query the content-type through the REST API: ```jsx { "data": null, "error": { "status": 403, "name": "PolicyError", "message": "The owner of the restaurant cannot submit reviews", "details": { "policy": "is-owner-review", "errCode": "RESTAURANT_OWNER_REVIEW" } } } ```

### Using custom errors on the front end **💭 Context:** Out of the box, the Next.js-powered front-end website provided with [FoodAdvisor](https://github.com/strapi/foodadvisor) does not display errors or success messages on the front-end website when accessing content. For instance, the website will not inform the user when adding a new review with a [previously created form](/cms/backend-customization/examples/services-and-controllers#rest-api-queries-from-the-front-end) is not possible. Let's say we want to customize the front end of FoodAdvisor to catch the custom error thrown by a [previously created custom policy](#creating-a-custom-policy) and display it to the user with a [React Hot Toast notification](https://github.com/timolins/react-hot-toast). As a bonus, another toast notification will be displayed when a review is successfully created.
Restaurant owner can't submit reviews
When the restaurant's owner tries to submit a new review, a custom error is returned with the REST API response and a toast notification is displayed on the front-end website.
**🎯 Goals**: - Catch the error on the front-end website and display it within a notification. - Send another notification in case the policy allows the creation of a new review. **🧑‍💻 Code example:** In the `/client` folder of the [FoodAdvisor](https://github.com/strapi/foodadvisor) project, you could update the [previously created `new-review` component](/cms/backend-customization/examples/services-and-controllers#rest-api-queries-from-the-front-end) as follows (modified lines are highlighted):
Example front-end code to display toast notifications for custom errors or successful review creation: ```jsx title="/client/components/pages/restaurant/RestaurantContent/Reviews/new-review.js" showLineNumbers // highlight-start /** * A notification will be displayed on the front-end using React Hot Toast * (See https://github.com/timolins/react-hot-toast). * React Hot Toast should be added to your project's dependencies; * Use yarn or npm to install it and it will be added to your package.json file. */ class UnauthorizedError extends Error { constructor(message) { super(message); } } // highlight-end const NewReview = () => { const router = useRouter(); const { handleSubmit, handleChange, values } = useFormik({ initialValues: { note: '', content: '', }, onSubmit: async (values) => { // highlight-start /** * The previously added code is wrapped in a try/catch block. */ try { // highlight-end const res = await fetch(getStrapiURL('/reviews'), { method: 'POST', body: JSON.stringify({ restaurant: router.query.slug, ...values, }), headers: { Authorization: `Bearer ${localStorage.getItem('token')}`, 'Content-Type': 'application/json', }, }); // highlight-start const { data, error } = await res.json(); /** * If the Strapi backend server returns an error, * we use the custom error message to throw a custom error. * If the request is a success, we display a success message. * In both cases, a toast notification is displayed on the front-end. */ if (error) { throw new UnauthorizedError(error.message); } toast.success('Review created!'); return data; } catch (err) { toast.error(err.message); console.error(err); } }, // highlight-end }); return (

Write your review

Send
); }; ```

:::strapi What's next? Learn more about how to configure [custom routes](/cms/backend-customization/examples/routes) to use your custom policies, and how these custom routes can be used to tweak a Strapi-based application. ::: # Custom routes Source: https://docs.strapi.io/cms/backend-customization/examples/routes # Examples cookbook: Custom routes Custom routes let you explicitly configure routes for content-types to control authentication and apply policies, such as bypassing default Strapi authentication or restricting access based on custom conditions. :::callout 🏗 About these examples These examples are built around [FoodAdvisor](https://github.com/strapi/foodadvisor), which is no longer Strapi's featured demo application (it has been superseded by [LaunchPad](https://github.com/strapi/LaunchPad)). What matters here is understanding the backend mechanisms being demonstrated, not FoodAdvisor itself. These pages will be revisited to use LaunchPad. ::: :::prerequisites This page is part of the back end customization examples cookbook. Please ensure you've read its [introduction](/cms/backend-customization/examples). ::: **💭 Context:** Out of the box, [FoodAdvisor](https://github.com/strapi/foodadvisor) does not control access to its content-type endpoints. Let's say we [previously created a policy](/cms/backend-customization/examples/policies) to restrict access to the "Reviews" content-type to some conditions, for instance to prevent a restaurant's owner to create a review for their restaurants. We must now enable the policy on the route we use to create reviews. **🎯 Goals**: - Explicitly define a routes configuration for the "Reviews" content-type. - Configure the route used when creating a review to: - bypass the default Strapi authentication system - and restrict access depending on the [previously defined custom policy](/cms/backend-customization/examples/policies). **Related concept** Additional information can be found in the [Policies](/cms/backend-customization/policies) and [Routes](/cms/backend-customization/routes) documentation. **🧑‍💻 Code example:** In the `/api` folder of the [FoodAdvisor](https://github.com/strapi/foodadvisor) project, replace the content of the `api/src/api/review/routes/review.js` file with the following code: ```jsx title="src/api/review/routes/review.js" 'use strict'; const { createCoreRouter } = require('@strapi/strapi').factories; module.exports = createCoreRouter('api::review.review', { config: { create: { auth: false, // set the route to bypass the normal Strapi authentication system policies: ['is-owner-review'], // set the route to use a custom policy middlewares: [], }, }, }); ```
:::strapi What's next? Learn more about how to configure [custom middlewares](/cms/backend-customization/examples/middlewares) to perform additional actions that extend your Strapi-based application. ::: # Custom services and controllers Source: https://docs.strapi.io/cms/backend-customization/examples/services-and-controllers # Examples cookbook: Custom services and controllers Services encapsulate reusable business logic that controllers invoke to handle reviews and email notifications. This guide demonstrates creating custom services using the Document Service API and custom controllers that call them. :::callout 🏗 About these examples These examples are built around [FoodAdvisor](https://github.com/strapi/foodadvisor), which is no longer Strapi's featured demo application (it has been superseded by [LaunchPad](https://github.com/strapi/LaunchPad)). What matters here is understanding the backend mechanisms being demonstrated, not FoodAdvisor itself. These pages will be revisited to use LaunchPad. ::: :::prerequisites This page is part of the back end customization examples cookbook. Please ensure you've read its [introduction](/cms/backend-customization/examples). ::: From the front-end website of [FoodAdvisor](https://github.com/strapi/foodadvisor), you can browse a list of restaurants accessible at [`localhost:3000/restaurants`](http://localhost:3000/restaurants). Clicking on any restaurant from the list will use the code included in the `/client` folder to display additional information about this restaurant. The content displayed on a restaurant page was created within Strapi's Content Manager and is retrieved by querying Strapi's REST API which uses code included in the `/api` folder. This page will teach about the following advanced topics: | Topic | Section | |------|---------| | Create a component that interacts with the backend of Strapi | [REST API queries from the front-end](#rest-api-queries-from-the-front-end) | | Understand how services and controllers can play together | [Controllers vs. services](#controllers-vs-services) | | Create custom services |
  • A [custom service](#custom-service-creating-a-review) that only uses the Document Service API
  • Another more [advanced custom service](#custom-service-sending-an-email-to-the-restaurant-owner) that uses both the Document Service API and a Strapi plugin
| | Use services in a controller | [Custom controller](#custom-controller) |
### REST API queries from the front end **💭 Context:** Restaurant pages on the front-end website of [FoodAdvisor](https://github.com/strapi/foodadvisor) include a Reviews section that is read-only. Adding reviews requires logging in to Strapi's admin panel and adding content to the "Reviews" collection type through the [Content Manager](/cms/features/content-manager). Let's add a small front-end component to restaurant pages. This component will allow a user to write a review directly from the front-end website.
Writing a review on the front end
A possible example of a form allowing users to submit a new review on a restaurant's page of the front-end website of FoodAdvisor
**🎯 Goals**: * Add a form to write a review. * Display the form on any restaurants page. * Send a POST request to Strapi's REST API when the form is submitted. * Use the [previously stored JWT](/cms/backend-customization/examples/authentication) to authenticate the request. **Related concept** Additional information on endpoints for content types can be found in the [REST API](/cms/api/rest#endpoints) documentation. **🧑‍💻 Code example:** In the `/client` folder of the [FoodAdvisor](https://github.com/strapi/foodadvisor) project, you could use the following code examples to: - create a new `pages/restaurant/RestaurantContent/Reviews/new-review.js` file, - and update the existing `components/pages/restaurant/RestaurantContent/Reviews/reviews.js`.
Example front-end code to add a component for writing reviews and display it on restaurants pages: 1. Create a new file in the `/client` folder to add a new component for writing reviews with the following code: ```jsx title='/client/components/pages/restaurant/RestaurantContent/Reviews/new-review.js' import { Button, Input, Textarea } from '@nextui-org/react'; import { useFormik } from 'formik'; import { useRouter } from 'next/router'; import React from 'react'; import { getStrapiURL } from '../../../../../utils'; const NewReview = () => { const router = useRouter(); const { handleSubmit, handleChange, values } = useFormik({ initialValues: { note: '', content: '', }, onSubmit: async (values) => { /** * Queries Strapi REST API to reach the reviews endpoint * using the JWT previously stored in localStorage to authenticate */ const res = await fetch(getStrapiURL('/reviews'), { method: 'POST', body: JSON.stringify({ restaurant: router.query.slug, ...values, }), headers: { Authorization: `Bearer ${localStorage.getItem('token')}`, 'Content-Type': 'application/json', }, }); }, }); /** * Renders the form */ return (

Write your review

Send
); }; export default NewReview; ``` 2. Display the new form component on any restaurants page by adding the highlighted lines (7, 8, and 13) to the code used to render restaurant's information: ```jsx title='/client/components/pages/restaurant/RestaurantContent/Reviews/reviews.js' showLineNumbers import React from 'react'; import delve from 'dlv'; import { formatDistance } from 'date-fns'; import { getStrapiMedia } from '../../../../../utils'; // highlight-start import { Textarea } from '@nextui-org/react'; import NewReview from './new-review'; // highlight-end const Reviews = ({ reviews }) => { return (
// highlight-next-line {reviews && reviews.map((review, index) => ( // … ```

### Controllers vs. Services Controllers could contain any business logic to be executed when the client requests a route. However, as your code grows bigger and becomes more structured, it is a best practice to split the logic into specific services that do only one thing well, then call the services from controllers. To illustrate the use of services, in this documentation the custom controller does not handle any responsibilities and delegates all the business logic to services. Let's say we would like to customize the back end of [FoodAdvisor](https://github.com/strapi/foodadvisor) to achieve the following scenario: when submitting the [previously added review form](#rest-api-queries-from-the-front-end) on the front-end website, Strapi will create a review in the back end and notify the restaurant owner by email. Translating this to Strapi back end customization means performing 3 actions: 1. Creating a custom service to [create the review](#custom-service-creating-a-review). 2. Creating a custom service to [send an email](#custom-service-sending-an-email-to-the-restaurant-owner). 3. [Customizing the default controller](#custom-controller) provided by Strapi for the Review content-type to use the 2 new services.
### Custom service: Creating a review **💭 Context:** By default, service files in Strapi includes basic boilerplate code that use the `createCoreService` factory function. Let's update the existing `review.js` service file for the "Reviews" collection type of [FoodAdvisor](https://github.com/strapi/foodadvisor) by replacing its code to create a review. **🎯 Goals**: - Declare a `create` method. - Grab context from the request. - Use the `findMany()` method from the Document Service API to find a restaurant. - Use the `create()` method from the Document Service API to append data to the restaurant, populating the restaurant owner. - Return the new review data. **Related concepts** Additional information can be found in the [request context](/cms/backend-customization/requests-responses), [services](/cms/backend-customization/services) and [Document Service API](/cms/api/document-service) documentation. **🧑‍💻 Code example:** To create such a service, in the `/api` folder of the [FoodAdvisor](https://github.com/strapi/foodadvisor) project, replace the content of the `src/api/review/services/review.js` file with the following code: ```jsx title="src/api/review/services/review.js" const { createCoreService } = require('@strapi/strapi').factories; module.exports = createCoreService('api::review.review', ({ strapi }) => ({ async create(ctx) { const user = ctx.state.user; const { body } = ctx.request; /** * Queries the Restaurants collection type * using the Document Service API * to retrieve information about the restaurant. */ const restaurants = await strapi.documents('api::restaurant.restaurant').findMany({ filters: { slug: body.restaurant, }, }); /** * Creates a new entry for the Reviews collection type * and populates data with information about the restaurant's owner * using the Document Service API. */ const newReview = await strapi.documents('api::review.review').create({ data: { note: body.note, content: body.content, restaurant: restaurants[0].documentId, author: user.id, }, populate: ['restaurant.owner'], }); return newReview; }, })); ``` :::tip Tips - In a controller's code, the `create` method from this service can be called with `strapi.service('api::review.review').create(ctx)` where `ctx` is the request's [context](/cms/backend-customization/requests-responses). - The provided example code does not cover error handling. You should consider handling errors, for instance when the restaurant does not exist. Additional information can be found in the [Error handling](/cms/error-handling) documentation. :::
### Custom Service: Sending an email to the restaurant owner **💭 Context:** Out of the box, [FoodAdvisor](https://github.com/strapi/foodadvisor) does not provide any automated email service feature. Let's create an `email.js` service file to send an email. We could use it in a [custom controller](#custom-controller) to notify the restaurant owner whenever a new review is created on the front-end website. :::callout 🤗 Optional service This service is an advanced code example using the [Email](/cms/features/email) plugin and requires understanding how plugins and providers work with Strapi. If you don't need an email service to notify the restaurant's owner, you can skip this part and jump next to the custom [controller](#custom-controller) example. ::: :::prerequisites - You have setup a [provider for the Email plugin](/cms/features/email), for instance the [Sendmail](https://www.npmjs.com/package/@strapi/provider-email-sendmail) provider. - In Strapi's admin panel, you have [created an `Email` single type](/cms/features/content-type-builder#creating-content-types) that contains a `from` Text field to define the sender email address. :::
Email Single Type in Admin Panel
An Email single type has been created in the admin panel. It contains a "from" field used to define the sender address for the Email plugin.
**🎯 Goals**: - Create a new service file for the "Email" single type, - Declare a `send()` method for this service, - Grab the sender address stored in the Email single type using the Document Service API, - Use email details (recipient's address, subject, and email body) passed when invoking the service's `send()` method to send an email using the Email plugin and a previously configured provider. **Related concepts** Additional information can be found in the [Services](/cms/backend-customization/services), [Document Service API](/cms/api/document-service) and [Email feature](/cms/features/email) documentation. **🧑‍💻 Code example:** To create such a service, in the `/api` folder of the [FoodAdvisor](https://github.com/strapi/foodadvisor) project, create a new `src/api/email/services/email.js` file with the following code: ```jsx title="src/api/email/services/email.js" const { createCoreService } = require('@strapi/strapi').factories; module.exports = createCoreService('api::email.email', ({ strapi }) => ({ async send({ to, subject, html }) { /** * Retrieves email configuration data * stored in the Email single type * using the Document Service API. * For a single type, use findFirst() to get its document. */ const emailConfig = await strapi.documents('api::email.email').findFirst(); /** * Sends an email using: * - parameters to pass when invoking the service * - the 'from' address previously retrieved with the email configuration */ await strapi.plugins['email'].services.email.send({ to, subject, html, from: emailConfig.from, }); }, })); ``` :::tip In a controller's code, the `send` method from this email service can be called with `strapi.service('api::email.email).send(parameters)` where `parameters` is an object with the email's related information (recipient's address, subject, and email body). :::
### Custom controller **💭 Context:** By default, controllers files in Strapi includes basic boilerplate code that use the `createCoreController` factory function. This exposes basic methods to create, retrieve, update, and delete content when reaching the requested endpoint. The default code for the controllers can be customized to perform any business logic. Let's customize the default controller for the "Reviews" collection type of [FoodAdvisor](https://github.com/strapi/foodadvisor) with the following scenario: upon a `POST` request to the `/reviews` endpoint, the controller calls previously created services to both [create a review](#custom-service-creating-a-review) and [send an email](#custom-service-sending-an-email-to-the-restaurant-owner) to the restaurant's owner. **🎯 Goals**: - Extend the existing controller for the "Reviews" collection type. - Declare a custom `create()` method. - Call previously created service(s). - Sanitize the content to be returned. **Related concept** Additional information can be found in the [controllers](/cms/backend-customization/controllers) documentation. **🧑‍💻 Code example:** In the `/api` folder of the [FoodAdvisor](https://github.com/strapi/foodadvisor) project, replace the content of the `src/api/review/controllers/review.js` file with one of the following code examples, depending on whether you previously created just [one custom service](#custom-service-creating-a-review) or both custom services for the review creation and the [email notification](#custom-service-sending-an-email-to-the-restaurant-owner): ```jsx title="src/api/review/controllers/review.js" const { createCoreController } = require('@strapi/strapi').factories; module.exports = createCoreController('api::review.review', ({ strapi }) => ({ /** * As the controller action is named * exactly like the original `create` action provided by the core controller, * it overwrites it. */ async create(ctx) { // Creates the new review using a service const newReview = await strapi.service('api::review.review').create(ctx); const sanitizedReview = await this.sanitizeOutput(newReview, ctx); ctx.body = sanitizedReview; }, })); ``` ```jsx title="src/api/review/controllers/review.js" const { createCoreController } = require('@strapi/strapi').factories; module.exports = createCoreController('api::review.review', ({ strapi }) => ({ /** * As the controller action is named * exactly like the original `create` action provided by the core controller, * it overwrites it. */ async create(ctx) { // Creates the new review using a service const newReview = await strapi.service('api::review.review').create(ctx); // Sends an email to the restaurant's owner, using another service if (newReview.restaurant?.owner) { await strapi.service('api::email.email').send({ to: newReview.restaurant.owner.email, subject: 'You have a new review!', html: `You've received a ${newReview.note} star review: ${newReview.content}`, }); } const sanitizedReview = await this.sanitizeOutput(newReview, ctx); ctx.body = sanitizedReview; }, })); ```
:::strapi What's next? Learn more about how [custom policies](/cms/backend-customization/examples/policies) can help you tweak a Strapi-based application and restrict access to some resources based on specific conditions. ::: # Customizing Users & Permissions plugin routes Source: https://docs.strapi.io/cms/backend-customization/guides/customizing-users-permissions-plugin-routes # Customizing Users & Permissions plugin routes [Users & Permissions](/cms/features/users-permissions) feature exposes `/users` and `/auth` routes that can be extended or overridden using the plugin extension system. This guide shows how to add custom policies, override controllers, and add new routes to the User collection. The Users & Permissions feature ships with built-in routes for authentication (`/auth`) and user management (`/users`). Because these routes belong to a plugin rather than a user-created content-type, they cannot be customized with `createCoreRouter`. Instead, extend them through the [plugin extension system](/cms/plugins-development/plugins-extension) using a `strapi-server` file in the `/src/extensions/users-permissions/` folder. :::prerequisites - A Strapi 5 project. - Familiarity with [routes](/cms/backend-customization/routes) and [policies](/cms/backend-customization/policies). ::: ## How it works [Users & Permissions](/cms/features/users-permissions) uses a route array and controller objects that differ from standard content-types. Understanding their structure is essential before customizing them. ### Route structure Unlike content-types you create (e.g., `api::restaurant.restaurant`), the Users & Permissions plugin registers its routes inside the `plugin.routes['content-api'].routes` array. This array contains all `/users`, `/auth`, `/roles`, and `/permissions` route definitions. Each route is an object with the following shape: ```js { method: 'GET', // HTTP method path: '/users', // URL path (relative to /api) handler: 'user.find', // controller.action config: { prefix: '', // path prefix (empty means /api) }, } ``` Route configurations can also include optional `policies` and `middlewares` arrays (see [Add a custom policy](#add-custom-policy)). ### The `strapi-server` extension file {#extend-routes} All customizations to the Users & Permissions plugin go in a single file: ```js title="/src/extensions/users-permissions/strapi-server.js" module.exports = (plugin) => { // Your customizations here return plugin; }; ``` ```ts title="/src/extensions/users-permissions/strapi-server.ts" // Your customizations here return plugin; }; ``` The function receives the full plugin object and must return the plugin. You can modify `plugin.routes`, `plugin.controllers`, `plugin.policies`, and `plugin.services` before returning. ### Available actions {#available-actions} The `user` controller is a plain object that exposes the following actions: | Action | Method | Path | Description | | ------ | ------ | ---- | ----------- | | `user.count` | `GET` | `/users/count` | Count users | | `user.find` | `GET` | `/users` | Find all users | | `user.me` | `GET` | `/users/me` | Get authenticated user | | `user.findOne` | `GET` | `/users/:id` | Find one user | | `user.create` | `POST` | `/users` | Create a user | | `user.update` | `PUT` | `/users/:id` | Update a user | | `user.destroy` | `DELETE` | `/users/:id` | Delete a user | The `auth` controller is a factory function `({ strapi }) => ({...})` that exposes the following actions: | Action | Method | Path | Rate limited | | ------ | ------ | ---- | ------------ | | `auth.callback` | `POST` | `/auth/local` | Yes | | `auth.callback` | `GET` | `/auth/:provider/callback` | No | | `auth.register` | `POST` | `/auth/local/register` | Yes | | `auth.connect` | `GET` | `/connect/(.*)` | Yes | | `auth.forgotPassword` | `POST` | `/auth/forgot-password` | Yes | | `auth.resetPassword` | `POST` | `/auth/reset-password` | Yes | | `auth.changePassword` | `POST` | `/auth/change-password` | Yes | | `auth.emailConfirmation` | `GET` | `/auth/email-confirmation` | No | | `auth.sendEmailConfirmation` | `POST` | `/auth/send-email-confirmation` | No | | `auth.refresh` | `POST` | `/auth/refresh` | No | | `auth.logout` | `POST` | `/auth/logout` | No | :::note Because the `user` and `auth` controllers have different types (plain object vs. factory function), they require different override patterns (see [Override a `user` controller action](#override-controller) and [Override an `auth` controller action](#override-auth-route)). ::: ## Customize routes {#customize-routes} You can add policies, register new endpoints, or remove existing ones by modifying the `plugin.routes['content-api'].routes` array in the extension file. ### Add a custom policy {#add-custom-policy} A common requirement is restricting who can update or delete user accounts: for example, ensuring users can only update their own profile. #### 1. Create the policy file Create a global policy that checks whether the authenticated user matches the target user. The policy function receives the Koa context (with access to `state.user` and `params`), an optional configuration object, and `{ strapi }`: ```js title="/src/policies/is-own-user.js" "use strict"; module.exports = (policyContext, config, { strapi }) => { const currentUser = policyContext.state.user; if (!currentUser) { return false; } const targetUserId = Number(policyContext.params.id); if (currentUser.id !== targetUserId) { return false; } return true; }; ``` ```ts title="/src/policies/is-own-user.ts" const currentUser = policyContext.state.user; if (!currentUser) { return false; } const targetUserId = Number(policyContext.params.id); if (currentUser.id !== targetUserId) { return false; } return true; }; ``` :::tip The `is-own-user` policy above applies specifically to Users & Permissions plugin routes. For a similar pattern on standard content-types (restricting access to the entry author), see the [is-owner middleware example](/cms/backend-customization/middlewares#restricting-content-access-with-an-is-owner-policy) and the [is-owner-review policy example](/cms/backend-customization/examples/policies#creating-a-custom-policy). ::: #### 2. Attach the policy to the user routes In the plugin extension file, find the `update` and `delete` routes and add the policy: ```js title="/src/extensions/users-permissions/strapi-server.js" module.exports = (plugin) => { // Find the routes that need the policy const routes = plugin.routes['content-api'].routes; // Add the 'is-own-user' policy to the update route const updateRoute = routes.find( (route) => route.handler === 'user.update' ); if (updateRoute) { updateRoute.config = updateRoute.config || {}; updateRoute.config.policies = updateRoute.config.policies || []; updateRoute.config.policies.push('global::is-own-user'); } // Add the same policy to the delete route const deleteRoute = routes.find( (route) => route.handler === 'user.destroy' ); if (deleteRoute) { deleteRoute.config = deleteRoute.config || {}; deleteRoute.config.policies = deleteRoute.config.policies || []; deleteRoute.config.policies.push('global::is-own-user'); } return plugin; }; ``` ```ts title="/src/extensions/users-permissions/strapi-server.ts" // Find the routes that need the policy const routes = plugin.routes['content-api'].routes; // Add the 'is-own-user' policy to the update route const updateRoute = routes.find( (route) => route.handler === 'user.update' ); if (updateRoute) { updateRoute.config = updateRoute.config || {}; updateRoute.config.policies = updateRoute.config.policies || []; updateRoute.config.policies.push('global::is-own-user'); } // Add the same policy to the delete route const deleteRoute = routes.find( (route) => route.handler === 'user.destroy' ); if (deleteRoute) { deleteRoute.config = deleteRoute.config || {}; deleteRoute.config.policies = deleteRoute.config.policies || []; deleteRoute.config.policies.push('global::is-own-user'); } return plugin; }; ``` With this configuration, `PUT /api/users/:id` and `DELETE /api/users/:id` return a `403 Forbidden` error if the authenticated user does not match the `:id` in the URL. :::tip For a more informative error message, throw a `PolicyError` instead of returning `false`: ```js const { errors } = require('@strapi/utils'); const { PolicyError } = errors; // Inside the policy: throw new PolicyError('You can only modify your own account'); ```
For more details on policy patterns and error handling, see the [policies documentation](/cms/backend-customization/policies). ::: ### Add a new route {#add-new-route} You can add custom routes to the Users & Permissions plugin. For example, add an endpoint that deactivates a user account as follows: ```js title="/src/extensions/users-permissions/strapi-server.js" module.exports = (plugin) => { // Add a new controller action plugin.controllers.user.deactivate = async (ctx) => { const { id } = ctx.params; const user = await strapi .plugin('users-permissions') .service('user') .edit(id, { blocked: true }); ctx.body = { message: `User ${user.username} has been deactivated` }; }; // Register the route plugin.routes['content-api'].routes.push({ method: 'POST', path: '/users/:id/deactivate', handler: 'user.deactivate', config: { prefix: '', policies: ['global::is-own-user'], }, }); return plugin; }; ``` ```ts title="/src/extensions/users-permissions/strapi-server.ts" // Add a new controller action plugin.controllers.user.deactivate = async (ctx) => { const { id } = ctx.params; const user = await strapi .plugin('users-permissions') .service('user') .edit(id, { blocked: true }); ctx.body = { message: `User ${user.username} has been deactivated` }; }; // Register the route plugin.routes['content-api'].routes.push({ method: 'POST', path: '/users/:id/deactivate', handler: 'user.deactivate', config: { prefix: '', policies: ['global::is-own-user'], }, }); return plugin; }; ``` After restarting Strapi, `POST /api/users/:id/deactivate` becomes available. Grant the corresponding permission in the admin panel under *Users & Permissions plugin > Roles* for the roles that should access this endpoint. ### Remove a route {#remove-route} You can disable a route by filtering it out of the routes array. For example, disable the user count endpoint as follows: ```js title="/src/extensions/users-permissions/strapi-server.js" module.exports = (plugin) => { plugin.routes['content-api'].routes = plugin.routes['content-api'].routes.filter( (route) => route.handler !== 'user.count' ); return plugin; }; ``` ```ts title="/src/extensions/users-permissions/strapi-server.ts" plugin.routes['content-api'].routes = plugin.routes['content-api'].routes.filter( (route) => route.handler !== 'user.count' ); return plugin; }; ``` ## Override controllers {#override-controllers} Beyond route-level customizations, you can override the controller actions themselves to change how the plugin handles requests. The `user` and `auth` controllers use different patterns, so each requires a specific approach. ### Override a `user` controller action {#override-controller} The `user` controller is a plain object, so you can directly read and replace its methods in the extension file. For instance, to add custom logic to the `me` endpoint: ```js title="/src/extensions/users-permissions/strapi-server.js" module.exports = (plugin) => { const originalMe = plugin.controllers.user.me; plugin.controllers.user.me = async (ctx) => { // Call the original controller await originalMe(ctx); // Add extra data to the response if (ctx.body) { ctx.body.timestamp = new Date().toISOString(); } }; return plugin; }; ``` ```ts title="/src/extensions/users-permissions/strapi-server.ts" const originalMe = plugin.controllers.user.me; plugin.controllers.user.me = async (ctx) => { // Call the original controller await originalMe(ctx); // Add extra data to the response if (ctx.body) { ctx.body.timestamp = new Date().toISOString(); } }; return plugin; }; ``` :::caution When wrapping a controller, always call the original function first to preserve the default behavior. Skipping the original function means you take over the full request handling, including sanitization and error handling. ::: ### Override an `auth` controller action {#override-auth-route} The `auth` controller uses a factory pattern: it exports a function `({ strapi }) => ({...})` instead of a plain object. When your extension code runs, Strapi has not yet resolved this factory. As a result, `plugin.controllers.auth` is a function, not an object with methods. To override an auth action, wrap the factory itself: ```js title="/src/extensions/users-permissions/strapi-server.js" module.exports = (plugin) => { const originalAuthFactory = plugin.controllers.auth; plugin.controllers.auth = ({ strapi }) => { // Resolve the original factory to get the controller methods const originalAuth = originalAuthFactory({ strapi }); // Override the register method const originalRegister = originalAuth.register; originalAuth.register = async (ctx) => { // Call the original register logic await originalRegister(ctx); // Custom post-registration logic if (ctx.body && ctx.body.user) { strapi.log.info(`New user registered: ${ctx.body.user.email}`); } }; return originalAuth; }; return plugin; }; ``` ```ts title="/src/extensions/users-permissions/strapi-server.ts" const originalAuthFactory = plugin.controllers.auth; plugin.controllers.auth = ({ strapi }) => { // Resolve the original factory to get the controller methods const originalAuth = originalAuthFactory({ strapi }); // Override the register method const originalRegister = originalAuth.register; originalAuth.register = async (ctx) => { // Call the original register logic await originalRegister(ctx); // Custom post-registration logic if (ctx.body && ctx.body.user) { strapi.log.info(`New user registered: ${ctx.body.user.email}`); } }; return originalAuth; }; return plugin; }; ``` :::caution Do not access `plugin.controllers.auth.register` directly. Because `auth` is a factory function at extension time, its methods are not accessible until Strapi calls the factory. Always wrap the factory as shown above. ::: ## Full example {#combine-customizations} The following example combines several customizations in a single file: it adds a policy to `update` and `delete`, wraps the `me` controller, and adds a new `profile` route. ```js title="/src/extensions/users-permissions/strapi-server.js" module.exports = (plugin) => { const routes = plugin.routes['content-api'].routes; // 1. Add 'is-own-user' policy to update and delete for (const route of routes) { if (route.handler === 'user.update' || route.handler === 'user.destroy') { route.config = route.config || {}; route.config.policies = route.config.policies || []; route.config.policies.push('global::is-own-user'); } } // 2. Wrap the 'me' controller to include the user's role const originalMe = plugin.controllers.user.me; plugin.controllers.user.me = async (ctx) => { await originalMe(ctx); if (ctx.state.user && ctx.body) { const user = await strapi .plugin('users-permissions') .service('user') .fetch(ctx.state.user.id, { populate: ['role'] }); ctx.body.role = user.role; } }; // 3. Add a custom route plugin.controllers.user.profile = async (ctx) => { const user = await strapi .plugin('users-permissions') .service('user') .fetch(ctx.state.user.id, { populate: ['role'] }); ctx.body = { username: user.username, email: user.email, role: user.role?.name, createdAt: user.createdAt, }; }; routes.push({ method: 'GET', path: '/users/profile', handler: 'user.profile', config: { prefix: '' }, }); return plugin; }; ``` ```ts title="/src/extensions/users-permissions/strapi-server.ts" const routes = plugin.routes['content-api'].routes; // 1. Add 'is-own-user' policy to update and delete for (const route of routes) { if (route.handler === 'user.update' || route.handler === 'user.destroy') { route.config = route.config || {}; route.config.policies = route.config.policies || []; route.config.policies.push('global::is-own-user'); } } // 2. Wrap the 'me' controller to include the user's role const originalMe = plugin.controllers.user.me; plugin.controllers.user.me = async (ctx) => { await originalMe(ctx); if (ctx.state.user && ctx.body) { const user = await strapi .plugin('users-permissions') .service('user') .fetch(ctx.state.user.id, { populate: ['role'] }); ctx.body.role = user.role; } }; // 3. Add a custom route plugin.controllers.user.profile = async (ctx) => { const user = await strapi .plugin('users-permissions') .service('user') .fetch(ctx.state.user.id, { populate: ['role'] }); ctx.body = { username: user.username, email: user.email, role: user.role?.name, createdAt: user.createdAt, }; }; routes.push({ method: 'GET', path: '/users/profile', handler: 'user.profile', config: { prefix: '' }, }); return plugin; }; ``` ## Validation After making changes, restart Strapi and verify your customizations: 1. Run `yarn strapi routes:list` to confirm your new or modified routes appear. 2. Test protected routes without authentication to verify policies return `403 Forbidden`. 3. Test with an authenticated user to confirm the expected behavior. 4. Check the Strapi server logs for errors during startup. ## Troubleshooting | Symptom | Possible cause | | ------- | -------------- | | Route not found (404) | The new route was not pushed to `plugin.routes['content-api'].routes`, or its `prefix` property is missing. | | Policy not applied | The policy name is incorrect. Global policies require the `global::` prefix (e.g., `global::is-own-user`). | | Controller returns 500 | The controller action name does not match the `handler` value in the route definition. | | Changes not reflected | Strapi was not restarted after modifying the extension file. Extensions are loaded at startup. | | Permission denied (403) | The new action is not enabled for the role. Enable it in *Users & Permissions plugin > Roles*. | | Cannot read property of `auth` controller | The `auth` controller is a factory function, not a plain object. Wrap the factory instead of accessing methods directly (see [Override an `auth` controller action](#override-auth-route)). | # Middlewares Source: https://docs.strapi.io/cms/backend-customization/middlewares # Middlewares customization Middlewares alter the request or response flow at application or API levels. This documentation distinguishes global versus route middlewares and illustrates custom implementations with generation patterns. :::strapi Different types of middlewares In Strapi, 3 middleware concepts coexist: - **Global middlewares** are [configured and enabled](/cms/configurations/middlewares) for the entire Strapi server application. These middlewares can be applied at the application level or at the API level.
The present documentation describes how to implement them.
Plugins can also add global middlewares (see [Server API documentation](/cms/plugins-development/server-api)). - **Route middlewares** have a more limited scope and are configured and used as middlewares at the route level. They are described in the [routes documentation](/cms/backend-customization/routes#middlewares). - **Document Service middlewares** apply to the Document Service API and have their own [implementation](/cms/api/document-service/middlewares) and related [lifecycle hooks](/cms/migration/v4-to-v5/breaking-changes/lifecycle-hooks-document-service#table). :::
Simplified Strapi backend diagram with global middlewares highlighted
The diagram represents a simplified version of how a request travels through the Strapi back end, with global middlewares highlighted. The backend customization introduction page includes a complete, interactive diagram.
## Implementation A new application-level or API-level middleware can be implemented: - with the [interactive CLI command `strapi generate`](/cms/cli#strapi-generate) - or manually by creating a JavaScript file in the appropriate folder (see [project structure](/cms/project-structure)): - `./src/middlewares/` for application-level middlewares - `./src/api/[api-name]/middlewares/` for API-level middlewares - `./src/plugins/[plugin-name]/middlewares/` for [plugin middlewares](/cms/plugins-development/server-policies-middlewares) Middlewares working with the REST API are functions like the following: ```js title="./src/middlewares/my-middleware.js or ./src/api/[api-name]/middlewares/my-middleware.js" module.exports = (config, { strapi })=> { return (context, next) => {}; }; ``` ```js title="./src/middlewares/my-middleware.js or ./src/api/[api-name]/middlewares/my-middleware.ts" return (context, next) => {}; }; ``` Globally scoped custom middlewares should be added to the [middlewares configuration file](/cms/configurations/middlewares#loading-order) or Strapi won't load them. API level and plugin middlewares can be added into the specific router that they are relevant to like the following: ```js title="./src/api/[api-name]/routes/[collection-name].js or ./src/plugins/[plugin-name]/server/routes/index.js" module.exports = { routes: [ { method: "GET", path: "/[collection-name]", handler: "[controller].find", config: { middlewares: ["[middleware-name]"], // See the usage section below for middleware naming conventions }, }, ], }; ```
Example of a custom timer middleware ```js title="/config/middlewares.js" module.exports = () => { return async (ctx, next) => { const start = Date.now(); await next(); const delta = Math.ceil(Date.now() - start); ctx.set('X-Response-Time', delta + 'ms'); }; }; ``` ```ts title="/config/middlewares.ts" return async (ctx, next) => { const start = Date.now(); await next(); const delta = Math.ceil(Date.now() - start); ctx.set('X-Response-Time', delta + 'ms'); }; }; ```
The GraphQL plugin also allows [implementing custom middlewares](/cms/plugins/graphql#middlewares), with a different syntax. :::tip Discover loaded middlewares Run `yarn strapi middlewares:list` to list all registered middlewares and double‑check naming when wiring them in routers. ::: ## Usage Middlewares are called different ways depending on their scope: - use `global::middleware-name` for application-level middlewares - use `api::api-name.middleware-name` for API-level middlewares - use `plugin::plugin-name.middleware-name` for plugin middlewares :::tip To list all the registered middlewares, run `yarn strapi middlewares:list`. ::: ### Restricting content access with an "is-owner policy" It is often required that the author of an entry is the only user allowed to edit or delete the entry. In previous versions of Strapi, this was known as an "is-owner policy". With Strapi v4, the recommended way to achieve this behavior is to use a middleware. Proper implementation largely depends on your project's needs and custom code, but the most basic implementation could be achieved with the following procedure: 1. From your project's folder, create a middleware with the Strapi CLI generator, by running the `yarn strapi generate` (or `npm run strapi generate`) command in the terminal. 2. Select `middleware` from the list, using keyboard arrows, and press Enter. 3. Give the middleware a name, for instance `isOwner`. 4. Choose `Add middleware to an existing API` from the list. 5. Select which API you want the middleware to apply. 6. Replace the code in the `/src/api/[your-api-name]/middlewares/isOwner.js` file with the following, replacing `api::restaurant.restaurant` in line 22 with the identifier corresponding to the API you choose at step 5 (e.g., `api::blog-post.blog-post` if your API name is `blog-post`): ```js showLineNumbers title="src/api/blog-post/middlewares/isOwner.js" "use strict"; /** * `isOwner` middleware */ module.exports = (config, { strapi }) => { // Add your own logic here. return async (ctx, next) => { const user = ctx.state.user; const entryId = ctx.params.id ? ctx.params.id : undefined; let entry = {}; /** * Gets all information about a given entry, * populating every relations to ensure * the response includes author-related information */ if (entryId) { entry = await strapi.documents('api::restaurant.restaurant').findOne( entryId, { populate: "*" } ); } /** * Compares user id and entry author id * to decide whether the request can be fulfilled * by going forward in the Strapi backend server */ if (user.id !== entry.author.id) { return ctx.unauthorized("This action is unauthorized."); } else { return next(); } }; }; ``` 7. Ensure the middleware is configured to apply on some routes. In the `config` object found in the `src/api/[your-api–name]/routes/[your-content-type-name].js` file, define the action keys (`find`, `findOne`, `create`, `update`, `delete`, etc.) for which you would like the middleware to apply, and declare the `isOwner` middleware for these routes.

For instance, if you wish to allow GET requests (mapping to the `find` and `findOne` actions) and POST requests (i.e., the `create` action) to any user for the `restaurant` content-type in the `restaurant` API, but would like to restrict PUT (i.e., `update` action) and DELETE requests only to the user who created the entry, you could use the following code in the `src/api/restaurant/routes/restaurant.js` file: ```js title="src/api/restaurant/routes/restaurant.js" /** * restaurant router */ const { createCoreRouter } = require("@strapi/strapi").factories; module.exports = createCoreRouter("api::restaurant.restaurant", { config: { update: { middlewares: ["api::restaurant.is-owner"], }, delete: { middlewares: ["api::restaurant.is-owner"], }, }, }); ``` :::info You can find more information about route middlewares in the [routes documentation](/cms/backend-customization/routes). ::: :::tip Middlewares for performance Route-level middlewares are a good place to centralize population logic and prevent accidental over-fetching. See [Building High-Performance Strapi Applications](https://strapi.io/blog/building-high-performance-strapi-applications-common-pitfalls-and-best-practices) on the Strapi blog for patterns and examples. ::: # Models Source: https://docs.strapi.io/cms/backend-customization/models # Models Models define Strapi’s content structure via content-types and reusable components. This documentation walks through creating these models in the Content-type Builder or CLI and managing schema files with optional lifecycle hooks. As Strapi is a headless Content Management System (CMS), creating a content structure for the content is one of the most important aspects of using the software. Models define a representation of the content structure. There are 2 different types of models in Strapi: - content-types, which can be collection types or single types, depending on how many entries they manage, - and components that are content structures re-usable in multiple content-types. If you are just starting out, it is convenient to generate some models with the [Content-type Builder](/cms/features/content-type-builder) directly in the admin panel. The user interface takes over a lot of validation tasks and showcases all the options available to create the content's content structure. The generated model mappings can then be reviewed at the code level using this documentation. ## Model creation Content-types and components models are created and stored differently. ### Content-types Content-types in Strapi can be created: - with the [Content-type Builder in the admin panel](/cms/features/content-type-builder), - or with [Strapi's interactive CLI `strapi generate`](/cms/cli#strapi-generate) command. The content-types use the following files: - `schema.json` for the model's [schema](#model-schema) definition. (generated automatically, when creating content-type with either method) - `lifecycles.js` for [lifecycle hooks](#lifecycle-hooks). This file must be created manually. These models files are stored in `./src/api/[api-name]/content-types/[content-type-name]/`, and any JavaScript or JSON file found in these folders will be loaded as a content-type's model (see [project structure](/cms/project-structure)). :::note In [TypeScript](/cms/typescript.md)-enabled projects, schema typings can be generated using the `ts:generate-types` command. ::: ### Components {#components-creation} Component models can't be created with CLI tools. Use the [Content-type Builder](/cms/features/content-type-builder) or create them manually. Components models are stored in the `./src/components` folder. Every component has to be inside a subfolder, named after the category the component belongs to (see [project structure](/cms/project-structure)). ## Model schema The `schema.json` file of a model consists of: - [settings](#model-settings), such as the kind of content-type the model represents or the table name in which the data should be stored, - [information](#model-information), mostly used to display the model in the admin panel and access it through the REST and GraphQL APIs, - [attributes](#model-attributes), which describe the content structure of the model, - and [options](#model-options) used to defined specific behaviors on the model. ### Model settings General settings for the model can be configured with the following parameters: | Parameter | Type | Description | | -------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------- | | `collectionName` | String | Database table name in which the data should be stored | | `kind`

_Optional,
only for content-types_ | String | Defines if the content-type is:
  • a collection type (`collectionType`)
  • or a single type (`singleType`)
| ```json // ./src/api/[api-name]/content-types/restaurant/schema.json { "kind": "collectionType", "collectionName": "Restaurants_v1", } ``` ### Model information The `info` key in the model's schema describes information used to display the model in the admin panel and access it through the Content API. It includes the following parameters: | Parameter | Type | Description | | -------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | `displayName` | String | Default name to use in the admin panel | | `singularName` | String | Singular form of the content-type name.
Used to generate the API routes and databases/tables collection.

Should be kebab-case. | | `pluralName` | String | Plural form of the content-type name.
Used to generate the API routes and databases/tables collection.

Should be kebab-case. | | `description` | String | Description of the model | ```json title="./src/api/[api-name]/content-types/restaurant/schema.json" "info": { "displayName": "Restaurant", "singularName": "restaurant", "pluralName": "restaurants", "description": "" }, ``` ### Model attributes The content structure of a model consists of a list of attributes. Each attribute has a `type` parameter, which describes its nature and defines the attribute as a simple piece of data or a more complex structure used by Strapi. Many types of attributes are available: - scalar types (e.g. strings, dates, numbers, booleans, etc.), - Strapi-specific types, such as: - `media` for files uploaded through the [Media library](/cms/features/content-type-builder#media) - `relation` to describe a [relation](#relations) between content-types - `customField` to describe [custom fields](#custom-fields) and their specific keys - `component` to define a [component](#components-json) (i.e. a content structure usable in multiple content-types) - `dynamiczone` to define a [dynamic zone](#dynamic-zones) (i.e. a flexible space based on a list of components) - and the `locale` and `localizations` types, only used by the [Internationalization (i18n) plugin](/cms/features/internationalization) The `type` parameter of an attribute should be one of the following values: | Type categories | Available types | |------|-------| | String types |
  • `string`
  • `text`
  • `richtext`
  • `enumeration`
  • `email`
  • `password`
  • [`uid`](#uid-type)
| | Date types |
  • `date`
  • `time`
  • `datetime`
  • `timestamp`
| | Number types |
  • `integer`
  • `biginteger`
  • `float`
  • `decimal`
| | Other generic types |
  • `boolean`
  • `json`
| | Special types unique to Strapi |
  • `media`
  • [`relation`](#relations)
  • [`customField`](#custom-fields)
  • [`component`](#components-json)
  • [`dynamiczone`](#dynamic-zones)
| | Internationalization (i18n)-related types

_Can only be used if the [i18n](/cms/features/internationalization) is enabled on the content-type_|
  • `locale`
  • `localizations`
| #### Validations Basic validations can be applied to attributes using the following parameters: | Parameter | Type | Description | Default | | -------------- | ------- | --------------------------------------------------------------------------------------------------------- | ------- | | `required` | Boolean | If `true`, adds a required validator for this property | `false` | | `max` | Integer | Checks if the value is greater than or equal to the given maximum | - | | `min` | Integer | Checks if the value is less than or equal to the given minimum | - | | `minLength` | Integer | Minimum number of characters for a field input value | - | | `maxLength` | Integer | Maximum number of characters for a field input value | - | | `private` | Boolean | If `true`, the attribute will be removed from the server response.

💡 This is useful to hide sensitive data. | `false` | | `configurable` | Boolean | If `false`, the attribute isn't configurable from the Content-type Builder plugin. | `true` | ```json title="./src/api/[api-name]/content-types/restaurant/schema.json" { // ... "attributes": { "title": { "type": "string", "minLength": 3, "maxLength": 99, "unique": true }, "description": { "default": "My description", "type": "text", "required": true }, "slug": { "type": "uid", "targetField": "title" } // ... } } ``` #### Database validations and settings :::caution 🚧 This API is considered experimental. These settings should be reserved to an advanced usage, as they might break some features. There are no plans to make these settings stable. ::: Database validations and settings are custom options passed directly onto the `tableBuilder` Knex.js function during schema migrations. Database validations allow for an advanced degree of control for setting custom column settings. The following options are set in a `column: {}` object per attribute: | Parameter | Type | Description | Default | | ------------- | ------- | --------------------------------------------------------------------------------------------- | ------- | | `name` | string | Changes the name of the column in the database | - | | `defaultTo` | string | Sets the database `defaultTo`, typically used with `notNullable` | - | | `notNullable` | boolean | Sets the database `notNullable`, ensures that columns cannot be null | `false` | | `unsigned` | boolean | Only applies to number columns, removes the ability to go negative but doubles maximum length | `false` | | `unique` | boolean | Enforces database-level uniqueness on published entries. Draft saves skip the check when Draft & Publish is enabled, so duplicates fail only at publish time. | `false` | | `type` | string | Changes the database type, if `type` has arguments, you should pass them in `args` | - | | `args` | array | Arguments passed into the Knex.js function that changes things like `type` | `[]` | :::caution Draft & Publish and `unique` When [Draft & Publish](/cms/features/draft-and-publish) is enabled, Strapi intentionally skips `unique` validations while an entry is saved as a draft. Duplicates therefore remain undetected until publication, at which point the database constraint triggers an error even though the UI previously displayed “Saved document” for the drafts. To avoid unexpected publication failures: - disable Draft & Publish on content-types that must stay globally unique, - or add custom validation (e.g. lifecycle hooks or middleware) that checks for draft duplicates before saving, - or rely on automatically generated unique identifiers such as a `uid` field and document editorial conventions. ::: ```json title="./src/api/[api-name]/content-types/restaurant/schema.json" { // ... "attributes": { "title": { "type": "string", "minLength": 3, "maxLength": 99, "unique": true, "column": { "unique": true // enforce database unique also } }, "description": { "default": "My description", "type": "text", "required": true, "column": { "defaultTo": "My description", // set database level default "notNullable": true // enforce required at database level, even for drafts } }, "rating": { "type": "decimal", "default": 0, "column": { "defaultTo": 0, "type": "decimal", // using the native decimal type but allowing for custom precision "args": [ 6,1 // using custom precision and scale ] } } // ... } } ``` #### `uid` type The `uid` type is used to automatically prefill the field value in the admin panel with a unique identifier (UID) (e.g. slugs for articles) based on 2 optional parameters: - `targetField` (string): If used, the value of the field defined as a target is used to auto-generate the UID. - `options` (string): If used, the UID is generated based on a set of options passed to [the underlying `uid` generator](https://github.com/sindresorhus/slugify). The resulting `uid` must match the following regular expression pattern: `/^[A-Za-z0-9-_.~]*$`. #### Relations Relations link content-types together. Strapi supports both single-entry relations (one-way and one-to-one) and multi relations where at least one side can point to several entries (one-to-many, many-to-one, many-to-many, and many-way). Multi relations are persisted as arrays in the database layer and are returned as arrays in the Content API responses. Relations are explicitly defined in the [attributes](#model-attributes) of a model with `type: 'relation'` and accept the following additional parameters: | Parameter | Description | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `relation` | The type of relation among these values:
  • `oneToOne`
  • `oneToMany`
  • `manyToOne`
  • `manyToMany`
| | `target` | Accepts a string value as the name of the target content-type | | `mappedBy` and `inversedBy`

_Optional_ | In bidirectional relations, the owning side declares the `inversedBy` key while the inversed side declares the `mappedBy` key | One-to-One relationships are useful when one entry can be linked to only one other entry. They can be unidirectional or bidirectional. In unidirectional relationships, only one of the models can be queried with its linked item.
Unidirectional use case example: - A blog article belongs to a category. - Querying an article can retrieve its category, - but querying a category won't retrieve the owned article. ```json title="./src/api/[api-name]/content-types/article/schema.json" // … attributes: { category: { type: 'relation', relation: 'oneToOne', target: 'category', }, }, // … ```
Bidirectional use case example: - A blog article belongs to a category. - Querying an article can retrieve its category, - and querying a category also retrieves its owned article. ```json title="./src/api/[api-name]/content-types/article/schema.json" // … attributes: { category: { type: 'relation', relation: 'oneToOne', target: 'category', inversedBy: 'article', }, }, // … ``` ```json title="./src/api/[api-name]/content-types/category/schema.json" // … attributes: { article: { type: 'relation', relation: 'oneToOne', target: 'article', mappedBy: 'category', }, }, // … ```
One-to-Many relationships are useful when: - an entry from a content-type A is linked to many entries of another content-type B, - while an entry from content-type B is linked to only one entry of content-type A. One-to-many relationships are always bidirectional, and are usually defined with the corresponding Many-to-One relationship:
Example: A person can own many plants, but a plant is owned by only one person. ```json title="./src/api/[api-name]/content-types/plant/schema.json" // … attributes: { owner: { type: 'relation', relation: 'manyToOne', target: 'api::person.person', inversedBy: 'plants', }, }, // … ``` ```json title="./src/api/person/models/schema.json" // … attributes: { plants: { type: 'relation', relation: 'oneToMany', target: 'api::plant.plant', mappedBy: 'owner', }, }, // … ```
Many-to-One relationships are useful to link many entries to one entry. They can be unidirectional or bidirectional. In unidirectional relationships, only one of the models can be queried with its linked item.
Unidirectional use case example: A book can be written by many authors. ```json title="./src/api/[api-name]/content-types/book/schema.json" // … attributes: { author: { type: 'relation', relation: 'manyToOne', target: 'author', }, }, // … ```
Bidirectional use case example: An article belongs to only one category but a category has many articles. ```json title="./src/api/[api-name]/content-types/article/schema.json" // … attributes: { author: { type: 'relation', relation: 'manyToOne', target: 'category', inversedBy: 'article', }, }, // … ``` ```json title="./src/api/[api-name]/content-types/category/schema.json" // … attributes: { books: { type: 'relation', relation: 'oneToMany', target: 'article', mappedBy: 'category', }, }, // … ```
Many-to-Many relationships are useful when: - an entry from content-type A is linked to many entries of content-type B, - and an entry from content-type B is also linked to many entries from content-type A. Many-to-many relationships can be unidirectional or bidirectional. In unidirectional relationships, only one of the models can be queried with its linked item.
Unidirectional use case example: ```json // … attributes: { categories: { type: 'relation', relation: 'manyToMany', target: 'category', }, }, // … ```
Bidirectional use case example: An article can have many tags and a tag can be assigned to many articles. ```json title="/src/api/[api-name]/content-types/article/schema.json" // … attributes: { tags: { type: 'relation', relation: 'manyToMany', target: 'tag', inversedBy: 'articles', }, }, // … ``` ```json title="./src/api/[api-name]/content-types/tag/schema.json" // … attributes: { articles: { type: 'relation', relation: 'manyToMany', target: 'article', mappedBy: 'tag', }, }, // … ```
#### Custom fields [Custom fields](/cms/features/custom-fields) extend Strapi’s capabilities by adding new types of fields to content-types. Custom fields are explicitly defined in the [attributes](#model-attributes) of a model with `type: customField`. Custom fields' attributes also show the following specificities: - a `customField` attribute whose value acts as a unique identifier to indicate which registered custom field should be used. Its value follows: - either the `plugin::plugin-name.field-name` format if a plugin created the custom field - or the `global::field-name` format for a custom field specific to the current Strapi application - and additional parameters depending on what has been defined when registering the custom field (see [custom fields documentation](/cms/features/custom-fields)). ```json title="./src/api/[apiName]/[content-type-name]/content-types/schema.json" { // … "attributes": { "attributeName": { // attributeName would be replaced by the actual attribute name "type": "customField", "customField": "plugin::color-picker.color", "options": { "format": "hex" } } } // … } ``` #### Components {#components-json} Component fields create a relation between a content-type and a component structure. Components are explicitly defined in the [attributes](#model-attributes) of a model with `type: 'component'` and accept the following additional parameters: | Parameter | Type | Description | | ------------ | ------- | ---------------------------------------------------------------------------------------- | | `repeatable` | Boolean | Could be `true` or `false` depending on whether the component is repeatable or not | | `component` | String | Define the corresponding component, following this format:
`.` | ```json title="./src/api/[apiName]/restaurant/content-types/schema.json" { "attributes": { "openinghours": { "type": "component", "repeatable": true, "component": "restaurant.openinghours" } } } ``` #### Dynamic zones Dynamic zones create a flexible space in which to compose content, based on a mixed list of [components](#components-json). Dynamic zones are explicitly defined in the [attributes](#model-attributes) of a model with `type: 'dynamiczone'`. They also accept a `components` array, where each component should be named following this format: `.`. ```json title="./src/api/[api-name]/content-types/article/schema.json" { "attributes": { "body": { "type": "dynamiczone", "components": ["article.slider", "article.content"] } } } ``` ### Model options The `options` key is used to define specific behaviors and accepts the following parameter: | Parameter | Type | Description | |---------------------|------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `privateAttributes` | Array of strings | Allows treating a set of attributes as private, even if they're not actually defined as attributes in the model. It could be used to remove them from API responses timestamps.

The `privateAttributes` defined in the model are merged with the `privateAttributes` defined in the global Strapi configuration. | | `draftAndPublish` | Boolean | Enables the draft and publish feature.

Default value: `true` (`false` if the content-type is created from the interactive CLI). | | `populateCreatorFields` | Boolean | Populates `createdBy` and `updatedBy` fields in responses returned by the REST API (see [guide](/cms/api/rest/guides/populate-creator-fields) for more details).

Default value: `false`. | ```json title="./src/api/[api-name]/content-types/restaurant/schema.json" { "options": { "privateAttributes": ["id", "createdAt"], "draftAndPublish": true } } ``` ### Plugin options `pluginOptions` is an optional object allowing plugins to store configuration for a model or a specific attribute. | Key | Value | Description | |---------------------------|-------------------------------|--------------------------------------------------------| | `i18n` | `localized: true` | Enables localization. | | `content-manager` | `visible: false` | Hides from Content Manager in the admin panel. | | `content-type-builder` | `visible: false` | Hides from Content-type Builder in the admin panel. | ```json title="./src/api/[api-name]/content-types/[content-type-name]/schema.json" { "attributes": { "name": { "pluginOptions": { "i18n": { "localized": true } }, "type": "string", "required": true }, "slug": { "pluginOptions": { "i18n": { "localized": true } }, "type": "uid", "targetField": "name", "required": true } // …additional attributes } } ``` ## Lifecycle hooks Lifecycle hooks are functions that get triggered when Strapi queries are called. They are triggered automatically when managing content through the administration panel or when developing custom code using `queries`· Lifecycle hooks can be customized declaratively or programmatically. :::caution Lifecycles hooks are not triggered when using directly the [knex](https://knexjs.org/) library instead of Strapi functions. ::: :::strapi Document Service API: lifecycles and middlewares The Document Service API triggers various database lifecycle hooks based on which method is called. For a complete reference, see [Document Service API: Lifecycle hooks](/cms/migration/v4-to-v5/breaking-changes/lifecycle-hooks-document-service#table). Bulk actions lifecycles (`createMany`, `updateMany`, `deleteMany`) will never be triggered by a Document Service API method. [Document Service middlewares](/cms/api/document-service/middlewares) can be implemented too. ::: ### Available lifecycle events The following lifecycle events are available: - `beforeCreate` - `beforeCreateMany` - `afterCreate` - `afterCreateMany` - `beforeUpdate` - `beforeUpdateMany` - `afterUpdate` - `afterUpdateMany` - `beforeDelete` - `beforeDeleteMany` - `afterDelete` - `afterDeleteMany` - `beforeCount` - `afterCount` - `beforeFindOne` - `afterFindOne` - `beforeFindMany` - `afterFindMany` ### Hook `event` object Lifecycle hooks are functions that take an `event` parameter, an object with the following keys: | Key | Type | Description | | -------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `action` | String | Lifecycle event that has been triggered (see [list](#available-lifecycle-events)) | | `model` | Array of strings (uid) | An array of uids of the content-types whose events will be listened to.
If this argument is not supplied, events are listened on all content-types. | | `params` | Object | Accepts the following parameters:
  • `data`
  • `select`
  • `where`
  • `orderBy`
  • `limit`
  • `offset`
  • `populate`
| | `result` | Object | _Optional, only available with `afterXXX` events_

Contains the result of the action. | | `state` | Object | Query state, can be used to share state between `beforeXXX` and `afterXXX` events of a query. | ### Declarative and programmatic usage To configure a content-type lifecycle hook, create a `lifecycles.js` file in the `./src/api/[api-name]/content-types/[content-type-name]/` folder. Each event listener is called sequentially. They can be synchronous or asynchronous. ```js title="./src/api/[api-name]/content-types/[content-type-name]/lifecycles.js" module.exports = { beforeCreate(event) { const { data, where, select, populate } = event.params; // let's do a 20% discount everytime event.params.data.price = event.params.data.price * 0.8; }, afterCreate(event) { const { result, params } = event; // do something to the result; }, }; ``` ```js title="./src/api/[api-name]/content-types/[content-type-name]/lifecycles.ts" beforeCreate(event) { const { data, where, select, populate } = event.params; // let's do a 20% discount everytime event.params.data.price = event.params.data.price * 0.8; }, afterCreate(event) { const { result, params } = event; // do something to the result; }, }; ``` Using the database layer API, it's also possible to register a subscriber and listen to events programmatically: ```js title="./src/index.js" module.exports = { async bootstrap({ strapi }) { // registering a subscriber strapi.db.lifecycles.subscribe({ models: [], // optional; beforeCreate(event) { const { data, where, select, populate } = event.params; event.state = 'doStuffAfterWards'; }, afterCreate(event) { if (event.state === 'doStuffAfterWards') { } const { result, params } = event; // do something to the result }, }); // generic subscribe for generic handling strapi.db.lifecycles.subscribe((event) => { if (event.action === 'beforeCreate') { // do something } }); } } ``` # Policies Source: https://docs.strapi.io/cms/backend-customization/policies # Policies Policies execute before controllers to enforce authorization or other checks on routes. Instructions in this documentation cover generating global or scoped policies and wiring them into router configs. Policies are functions that execute specific logic on each request before it reaches the [controller](/cms/backend-customization/controllers). They are mostly used for securing business logic. Each [route](/cms/backend-customization/routes) of a Strapi project can be associated to an array of policies. For example, a policy named `is-admin` could check that the request is sent by an admin user, and restrict access to critical routes. Policies can be global or scoped. [Global policies](#global-policies) can be associated to any route in the project. Scoped policies only apply to a specific [API](#api-policies) or [plugin](#plugin-policies) and should live under the corresponding `./src/api//policies/` or `./src/plugins//policies/` folder.
Simplified Strapi backend diagram with routes and policies highlighted
The diagram represents a simplified version of how a request travels through the Strapi back end, with policies and routes highlighted. The backend customization introduction page includes a complete, interactive diagram.
## Implementation A new policy can be implemented: - with the [interactive CLI command `strapi generate`](/cms/cli#strapi-generate) - or manually by creating a JavaScript file in the appropriate folder (see [project structure](/cms/project-structure)): - `./src/policies/` for global policies - `./src/api/[api-name]/policies/` for API policies - `./src/plugins/[plugin-name]/policies/` for plugin policies
Global policy implementation example: ```js title="./src/policies/is-authenticated.js" module.exports = (policyContext, config, { strapi }) => { if (policyContext.state.user) { // if a session is open // go to next policy or reach the controller's action return true; } return false; // If you return nothing, Strapi considers you didn't want to block the request and will let it pass }; ``` ```ts title="./src/policies/is-authenticated.ts" if (policyContext.state.user) { // if a session is open // go to next policy or reach the controller's action return true; } return false; // If you return nothing, Strapi considers you didn't want to block the request and will let it pass }; ``` `policyContext` is a wrapper around the [controller](/cms/backend-customization/controllers) context. It adds some logic that can be useful to implement a policy for both REST and GraphQL.
Policies can be configured using a `config` object: ```js title="./src/api/[api-name]/policies/my-policy.js" module.exports = (policyContext, config, { strapi }) => { if (policyContext.state.user.role.code === config.role) { // if user's role is the same as the one described in configuration return true; } return false; // If you return nothing, Strapi considers you didn't want to block the request and will let it pass }; ``` ```ts title="./src/api/[api-name]/policies/my-policy.ts" if (policyContext.state.user.role.code === config.role) { // if user's role is the same as the one described in configuration return true; } return false; // If you return nothing, Strapi considers you didn't want to block the request and will let it pass }; ``` ## Usage To apply policies to a route, add them to its configuration object (see [routes documentation](/cms/backend-customization/routes#policies)). Policies are called different ways depending on their scope: - use `global::policy-name` for [global policies](#global-policies) - use `api::api-name.policy-name` for [API policies](#api-policies) - use `plugin::plugin-name.policy-name` for [plugin policies](#plugin-policies) :::tip To list all the available policies, run `yarn strapi policies:list`. ::: ### Global policies Global policies can be associated to any route in a project. ```js title="./src/api/restaurant/routes/custom-restaurant.js" module.exports = { routes: [ { method: 'GET', path: '/restaurants', handler: 'Restaurant.find', config: { /** Before executing the find action in the Restaurant.js controller, we call the global 'is-authenticated' policy, found at ./src/policies/is-authenticated.js. */ policies: ['global::is-authenticated'] } } ] } ``` ```ts title="./src/api/restaurant/routes/custom-restaurant.ts" routes: [ { method: 'GET', path: '/restaurants', handler: 'Restaurant.find', config: { /** Before executing the find action in the Restaurant.js controller, we call the global 'is-authenticated' policy, found at ./src/policies/is-authenticated.js. */ policies: ['global::is-authenticated'] } } ] } ``` ### Plugin policies Plugins can add and expose policies to an application. For example, the [Users & Permissions feature](/cms/features/users-permissions) comes with policies to ensure that the user is authenticated or has the rights to perform an action: ```js title="./src/api/restaurant/routes/custom-restaurant.js" module.exports = { routes: [ { method: 'GET', path: '/restaurants', handler: 'Restaurant.find', config: { /** The `isAuthenticated` policy prodived with the `users-permissions` plugin is executed before the `find` action in the `Restaurant.js` controller. */ policies: ['plugin::users-permissions.isAuthenticated'] } } ] } ``` ```ts title="./src/api/restaurant/routes/custom-restaurant.ts" routes: [ { method: 'GET', path: '/restaurants', handler: 'Restaurant.find', config: { /** The `isAuthenticated` policy prodived with the `users-permissions` plugin is executed before the `find` action in the `Restaurant.js` controller. */ policies: ['plugin::users-permissions.isAuthenticated'] } } ] } ``` ### API policies API policies are associated to the routes defined in the API where they have been declared. ```js title="./src/api/restaurant/policies/is-admin.js." module.exports = async (policyContext, config, { strapi }) => { if (policyContext.state.user.role.name === 'Administrator') { // Go to next policy or will reach the controller's action. return true; } return false; }; ``` ```js title="./src/api/restaurant/routes/custom-restaurant.js" module.exports = { routes: [ { method: 'GET', path: '/restaurants', handler: 'Restaurant.find', config: { /** The `is-admin` policy found at `./src/api/restaurant/policies/is-admin.js` is executed before the `find` action in the `Restaurant.js` controller. */ policies: ['is-admin'] } } ] } ``` ```ts title="./src/api/restaurant/policies/is-admin.ts" if (policyContext.state.user.role.name === 'Administrator') { // Go to next policy or will reach the controller's action. return true; } return false; }; ``` ```ts title="./src/api/restaurant/routes/custom-restaurant.ts" routes: [ { method: 'GET', path: '/restaurants', handler: 'Restaurant.find', config: { /** The `is-admin` policy found at `./src/api/restaurant/policies/is-admin.js` is executed before the `find` action in the `Restaurant.ts` controller. */ policies: ['is-admin'] } } ] } ``` To use a policy in another API, reference it with the following syntax: `api::[apiName].[policyName]`: ```js title="./src/api/category/routes/custom-category.js" module.exports = { routes: [ { method: 'GET', path: '/categories', handler: 'Category.find', config: { /** The `is-admin` policy found at `./src/api/restaurant/policies/is-admin.js` is executed before the `find` action in the `Restaurant.js` controller. */ policies: ['api::restaurant.is-admin'] } } ] } ``` ```ts title="./src/api/category/routes/custom-category.ts" routes: [ { method: 'GET', path: '/categories', handler: 'Category.find', config: { /** The `is-admin` policy found at `./src/api/restaurant/policies/is-admin.ts` is executed before the `find` action in the `Restaurant.js` controller. */ policies: ['api::restaurant.is-admin'] } } ] } ``` # Requests and Responses Source: https://docs.strapi.io/cms/backend-customization/requests-responses # Requests and Responses Koa’s context (`ctx`) carries request info, state, and response data through every Strapi endpoint. This documentation details `ctx.request`, `ctx.state`, and `ctx.response`, plus a helper for accessing context anywhere. The Strapi back end server is based on [Koa](https://koajs.com/). When you send requests through the [REST API](/cms/api/rest), a context object (`ctx`) is passed to every element of the Strapi back end (e.g., [policies](/cms/backend-customization/policies), [controllers](/cms/backend-customization/controllers), [services](/cms/backend-customization/services)). `ctx` includes 3 main objects: - [`ctx.request`](#ctxrequest) for information about the request sent by the client making an API request, - [`ctx.state`](#ctxstate) for information about the state of the request within the Strapi back end, - and [`ctx.response`](#ctxresponse) for information about the response that the server will return. :::tip The request's context can also be accessed from anywhere in the code with the [`strapi.requestContext` function](#accessing-the-request-context-anywhere). ::: :::info In addition to the concepts and parameters described in the following documentation, you might find additional information in the [Koa request documentation](http://koajs.com/#request), [Koa Router documentation](https://github.com/koajs/router/blob/master/API.md) and [Koa response documentation](http://koajs.com/#response). :::
Simplified Strapi backend diagram with requests and responses highlighted
The diagram represents a simplified version of how a request travels through the Strapi back end, with requests and responses highlighted. The backend customization introduction page includes a complete, interactive diagram.
## `ctx.request` The `ctx.request` object contains the following parameters: | Parameter | Description | Type | | --------------------- | -------------------------------------------------------------------------------------------- | -------- | | `ctx.request.body` | Parsed version of the body. | `Object` | | `ctx.request.files` | Files sent with the request. | `Array` | | `ctx.request.headers` | Headers sent with the request. | `Object` | | `ctx.request.host` | Host part of the URL, including the port. | `String` | | `ctx.request.hostname`| Host part of the URL, excluding the port. | `String` | | `ctx.request.href` | Complete URL of the requested resource, including the protocol, domain, port (if specified), path, and query parameters. | `String` | | `ctx.request.ip` | IP of the person sending the request.| `String` | | `ctx.request.ips` | When `X-Forwarded-For` is present and `app.proxy` is enabled, an array of IPs is returned, ordered from upstream to downstream.

For example if the value were "client, proxy1, proxy2", you would receive the `["client", "proxy1", "proxy2"]` array. | `Array` | | `ctx.request.method` | Request method (e.g., `GET`, `POST`). | `String` | | `ctx.request.origin` | URL part before the first `/`. | `String` | | `ctx.request.params` | Parameters sent in the URL.

For example, if the internal URL is `/restaurants/:id`, whatever you replace `:id` in the real request becomes accessible through `ctx.request.params.id`. | `Object` | | `ctx.request.path` | Path of the requested resource, excluding the query parameters. | `String` | | `ctx.request.protocol`| Protocol being used (e.g., `https` or `http`). | `String` | | `ctx.request.query` | Strapi-specific [query parameters](#ctxrequestquery). | `Object` | | `ctx.request.subdomains`| Subdomains included in the URL.

For example, if the domain is `tobi.ferrets.example.com`, the value is the following array: `["ferrets", "tobi"]`. | `Array` | | `ctx.request.url` | Path and query parameters of the requested resource, excluding the protocol, domain, and port. | `String` |
Differences between protocol, origin, url, href, path, host, and hostname : Given an API request sent to the `https://example.com:1337/api/restaurants?id=123` URL, here is what different parameters of the `ctx.request` object return: | Parameter | Returned value | | ---------- | ------------------------------------------------- | | `ctx.request.href` | `https://example.com:1337/api/restaurants?id=123` | | `ctx.request.protocol` | `https` | | `ctx.request.host` | `localhost:1337` | | `ctx.request.hostname` | `localhost` | | `ctx.request.origin` | `https://example.com:1337` | | `ctx.request.url` | `/api/restaurants?id=123` | | `ctx.request.path` | `/api/restaurants` |
### `ctx.request.query` `ctx.request` provides a `query` object that gives access to Strapi query parameters. The following table lists available parameters with a short description and a link to the relevant REST API documentation section (see [REST API parameters](/cms/api/rest/parameters) for more information): | Parameter | Description | Type | | -------------------------------------| --------------------------------------------------------------------------------------------------------------------------- | -------------------- | | `ctx.request.query`
`ctx.query` | The whole query object. | `Object` | | `ctx.request.query.sort` | Parameters to [sort the response](/cms/api/rest/sort-pagination.md#sorting) | `String` or `Array` | | `ctx.request.query.filters` | Parameters to [filter the response](/cms/api/rest/filters) | `Object` | | `ctx.request.query.populate` | Parameters to [populate relations, components, or dynamic zones](/cms/api/rest/populate-select#population) | `String` or `Object` | | `ctx.request.query.fields` | Parameters to [select only specific fields to return with the response](/cms/api/rest/populate-select#field-selection) | `Array` | | `ctx.request.query.pagination` | Parameter to [page through entries](/cms/api/rest/sort-pagination.md#pagination) | `Object` | | `ctx.request.query.publicationState` | Parameter to [select the Draft & Publish state](/cms/api/rest/status) | `String` | | `ctx.request.query.locale` | Parameter to [select one or multiple locales](/cms/api/rest/locale) | `String` or `Array` | ## `ctx.state` The `ctx.state` object gives access to the state of the request within the Strapi back end, including specific values about the [user](#ctxstateuser), [authentication](#ctxstateauth), [route](#ctxstateroute): | Parameter | Description | Type | | ---------------------------|---------------------------------------------------------------------------- | -------- | | `ctx.state.isAuthenticated`| Returns whether the current user is authenticated in any way. | `Boolean` | ### `ctx.state.user` The `ctx.state.user` object gives access to information about the user performing the request and includes the following parameters: | Parameter | Description | Type | | ----------| -------------------------------------------------------------------------------------------- | -------- | | `ctx.state.user`| User's information. Only one relation is populated. | `Object` | | `ctx.state.user.role`| The user's role | `Object` | ### `ctx.state.auth` The `ctx.state.auth` object gives access to information related to the authentication and includes the following parameters: | Parameter | Description | Type | | ------------------------------| -------------------------------------------------------------------------------------------- | -------- | | `ctx.state.auth.strategy` | Information about the currently used authentication strategy ([Users & Permissions plugin](/cms/features/users-permissions) or [API tokens](/cms/features/api-tokens)) | `Object` | | `ctx.state.auth.strategy.name`| Name of the currently used strategy | `String` | | `ctx.state.auth.credentials` | The user's credentials | `String` | ### `ctx.state.route` The `ctx.state.route` object gives access to information related to the current route and includes the following parameters: | Parameter | Description | Type | | ----------| -------------------------------------------------------------------------------------------- | -------- | | `ctx.state.route.method`| Method used to access the current route. | `String` | | `ctx.state.route.path`| Path of the current route. | `String` | | `ctx.state.route.config`| Configuration information about the current route. | `Object` | | `ctx.state.route.handler`| Handler (controller) of the current route. | `Object` | | `ctx.state.route.info`| Additional information about the current route, such as the apiName and the API request type. | `Object` | | `ctx.state.route.info.apiName`| Name of the used API. | `String` | | `ctx.state.route.info.type`| Type of the used API. | `String` | ## `ctx.response` The `ctx.response` object gives access to information related to the response that the server will return and includes the following parameters: | Parameter | Description | Type | | ----------| -------------------------------------------------------------------------------------------- | -------- | | `ctx.response.body`| Body of the response. | `Any` | | `ctx.response.status` | Status code of the response. | `Integer` | | `ctx.response.message`| Status message of the response.

By default, `response.message` is associated with `response.status`. | `String` | | `ctx.response.header`
`ctx.response.headers`| Header(s) sent with the response. | `Object` | | `ctx.response.length`| [`Content-Length`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length) header value as a number when present, or deduces it from `ctx.body` when possible; otherwise, returns `undefined`. | `Integer` | | `ctx.response.redirect`
`ctx.response.redirect(url, [alt])` | Performs a `302` redirect to the URL. The string "back" is special-cased to provide Referrer support; when Referrer is not present, alt or "/" is used.

Example: `ctx.response.redirect('back', '/index.html');` | `Function` | | `ctx.response.attachment`

`ctx.response.attachment([filename], [options])` | Sets [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header to "attachment" to signal the client to prompt for download. Optionally specify the filename of the download and some [options](https://github.com/jshttp/content-disposition#options). | `Function` | | `ctx.response.type`| [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) header, void of parameters such as "charset". | `String` | | `ctx.response.lastModified`| [`Last-Modified`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified) header as a Date, if it exists. | `DateTime` | | `ctx.response.etag`| Sets the [`ETag`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) of a response including the wrapped "s.
There is no corresponding `response.etag` getter. | `String` | ## Accessing the request context anywhere Strapi exposes a way to access the current request context from anywhere in the code (e.g. lifecycle functions). You can access the request as follows: ```js const ctx = strapi.requestContext.get(); ``` You should only use this inside of functions that will be called in the context of an HTTP request. ```js // correct const service = { myFunction() { const ctx = strapi.requestContext.get(); console.log(ctx.state.user); }, }; // incorrect const ctx = strapi.requestContext.get(); const service = { myFunction() { console.log(ctx.state.user); }, }; ``` **Example:** ```js title="./api/test/content-types/article/lifecycles.js" module.exports = { beforeUpdate() { const ctx = strapi.requestContext.get(); console.log('User info in service: ', ctx.state.user); }, }; ``` :::note Strapi uses a Node.js feature called [AsyncLocalStorage](https://nodejs.org/docs/latest-v16.x/api/async_context.html#class-asynclocalstorage) to make the context available anywhere. ::: :::tip For making authenticated HTTP requests from the admin panel in plugins, see [Admin Panel API: Fetch client](/cms/plugins-development/admin-fetch-client). ::: # Routes Source: https://docs.strapi.io/cms/backend-customization/routes # Routes Routes map incoming URLs to controllers and ship pre-generated for each content type. This documentation shows how to add or customize core and custom routers and attach policies or middlewares for extra control. Requests sent to Strapi on any URL are handled by routes. By default, Strapi generates routes for all the content-types (see [REST API documentation](/cms/api/rest)). Routes can be [added](#implementation) and configured: - with [policies](#policies), which are a way to block access to a route, - and with [middlewares](#middlewares), which are a way to control and change the request flow and the request itself. Once a route exists, reaching it executes some code handled by a controller (see [controllers documentation](/cms/backend-customization/controllers)). To view all existing routes and their hierarchal order, you can run `yarn strapi routes:list` (see [CLI reference](/cms/cli)). :::tip If you only customize the default controller actions (`find`, `findOne`, `create`, `update`, or `delete`) that Strapi generates for a content-type, you can leave the router as-is. Those core routes already target the same handler names and will run your new controller logic. Add or edit a route only when you need a brand-new HTTP path/method or want to expose a custom controller action. :::
Simplified Strapi backend diagram with routes highlighted
The diagram represents a simplified version of how a request travels through the Strapi back end, with routes highlighted. The backend customization introduction page includes a complete, interactive diagram.
## Implementation Implementing a new route consists in defining it in a router file within the `./src/api/[apiName]/routes` folder (see [project structure](/cms/project-structure)). There are 2 different router file structures, depending on the use case: - configuring [core routers](#configuring-core-routers) - or creating [custom routers](#creating-custom-routers). ### Configuring core routers Core routers (i.e. `find`, `findOne`, `create`, `update`, and `delete`) correspond to [default routes](/cms/api/rest#endpoints) automatically created by Strapi when a new [content-type](/cms/backend-customization/models#model-creation) is created. Strapi provides a `createCoreRouter` factory function that automatically generates the core routers and allows: - passing in configuration options to each router - and disabling some core routers to [create custom ones](#creating-custom-routers). A core router file is a JavaScript file exporting the result of a call to `createCoreRouter` with the following parameters: | Parameter | Description | Type | | ----------| -------------------------------------------------------------------------------------------- | -------- | | `prefix` | Allows passing in a custom prefix to add to all routers for this model (e.g. `/test`) | `String` | | `only` | Core routes that will only be loaded

Anything not in this array is ignored. | `Array` | --> | `except` | Core routes that should not be loaded

This is functionally the opposite of the `only` parameter. | `Array` | | `config` | Configuration to handle [policies](#policies), [middlewares](#middlewares) and [public availability](#public-routes) for the route | `Object` |
```js title="./src/api/[apiName]/routes/[routerName].js (e.g './src/api/restaurant/routes/restaurant.js')" const { createCoreRouter } = require('@strapi/strapi').factories; module.exports = createCoreRouter('api::restaurant.restaurant', { prefix: '', only: ['find', 'findOne'], except: [], config: { find: { auth: false, policies: [], middlewares: [], }, findOne: {}, create: {}, update: {}, delete: {}, }, }); ``` ```ts title="./src/api/[apiName]/routes/[routerName].ts (e.g './src/api/restaurant/routes/restaurant.ts')" prefix: '', only: ['find', 'findOne'], except: [], config: { find: { auth: false, policies: [], middlewares: [], }, findOne: {}, create: {}, update: {}, delete: {}, }, }); ```
Generic implementation example: ```js title="./src/api/restaurant/routes/restaurant.js" const { createCoreRouter } = require('@strapi/strapi').factories; module.exports = createCoreRouter('api::restaurant.restaurant', { only: ['find'], config: { find: { auth: false, policies: [], middlewares: [], } } }); ``` ```ts title="./src/api/restaurant/routes/restaurant.ts" only: ['find'], config: { find: { auth: false, policies: [], middlewares: [], } } }); ``` This only allows a `GET` request on the `/restaurants` path from the core `find` [controller](/cms/backend-customization/controllers) without authentication. When you reference custom controller actions in custom routers, prefer the fully-qualified `api::..` form for clarity (e.g., `api::restaurant.restaurant.review`). ### Creating custom routers Creating custom routers consists in creating a file that exports an array of objects, each object being a route with the following parameters: | Parameter | Description | Type | | -------------------------- | -------------------------------------------------------------------------------- | -------- | | `method` | Method associated to the route (i.e. `GET`, `POST`, `PUT`, `DELETE` or `PATCH`) | `String` | | `path` | Path to reach, starting with a forward-leading slash (e.g. `/articles`)| `String` | | `handler` | Function to execute when the route is reached.
Use the fully-qualified syntax `api::api-name.controllerName.actionName` (or `plugin::plugin-name.controllerName.actionName`). The short `.` form for legacy projects also works. | `String` | | `config`

_Optional_ | Configuration to handle [policies](#policies), [middlewares](#middlewares) and [public availability](#public-routes) for the route

| `Object` |
Dynamic routes can be created using parameters and regular expressions. These parameters will be exposed in the `ctx.params` object. For more details, please refer to the [PathToRegex](https://github.com/pillarjs/path-to-regexp) documentation. :::caution Routes files are loaded in alphabetical order. To load custom routes before core routes, make sure to name custom routes appropriately (e.g. `01-custom-routes.js` and `02-core-routes.js`). ::: :::info Controller handler naming reference The `handler` string acts as a pointer to the controller action that should run for the route. Strapi supports the following formats: - API controllers: `api::..` (e.g. `api::restaurant.restaurant.exampleAction`). The `` comes from the controller filename inside `./src/api//controllers/`. - Plugin controllers: `plugin::..` when the controller lives in a plugin. For backwards compatibility, Strapi also accepts a short `.` string for API controllers, but using the fully-qualified form makes the route more explicit and avoids naming collisions across APIs and plugins. :::
Example of a custom router using URL parameters and regular expressions for routes In the following example, the custom routes file name is prefixed with `01-` to make sure the route is reached before the core routes. ```js title="./src/api/restaurant/routes/01-custom-restaurant.js" /** @type {import('@strapi/strapi').Core.RouterConfig} */ const config = { type: 'content-api', routes: [ { // Path defined with an URL parameter method: 'POST', path: '/restaurants/:id/review', handler: 'api::restaurant.restaurant.review', }, { // Path defined with a regular expression method: 'GET', path: '/restaurants/:category([a-z]+)', // Only match when the URL parameter is composed of lowercase letters handler: 'api::restaurant.restaurant.findByCategory', } ] } module.exports = config ``` ```js title="./src/api/restaurant/routes/01-custom-restaurant.ts" const config: Core.RouterConfig = { type: 'content-api', routes: [ { // Path defined with a URL parameter method: 'GET', path: '/restaurants/:category/:id', handler: 'api::restaurant.restaurant.findOneByCategory', }, { // Path defined with a regular expression method: 'GET', path: '/restaurants/:region(\\d{2}|\\d{3})/:id', // Only match when the first parameter contains 2 or 3 digits. handler: 'api::restaurant.restaurant.findOneByRegion', } ] } ```
## Configuration Both [core routers](#configuring-core-routers) and [custom routers](#creating-custom-routers) have the same configuration options. The routes configuration is defined in a `config` object that can be used to handle [policies](#policies) and [middlewares](#middlewares) or to [make the route public](#public-routes). ### Policies [Policies](/cms/backend-customization/policies) can be added to a route configuration: - by pointing to a policy registered in `./src/policies`, with or without passing a custom configuration - or by declaring the policy implementation directly, as a function that takes `policyContext` to extend [Koa](https://koajs.com/#context) (`ctx`) and the `strapi` instance as arguments (see [policies documentation](/cms/backend-customization/routes)) ```js title="./src/api/restaurant/routes/restaurant.js" const { createCoreRouter } = require('@strapi/strapi').factories; module.exports = createCoreRouter('api::restaurant.restaurant', { config: { find: { policies: [ // point to a registered policy 'policy-name', // point to a registered policy with some custom configuration { name: 'policy-name', config: {} }, // pass a policy implementation directly (policyContext, config, { strapi }) => { return true; }, ] } } }); ``` ```js title="./src/api/restaurant/routes/restaurant.ts" config: { find: { policies: [ // point to a registered policy 'policy-name', // point to a registered policy with some custom configuration { name: 'policy-name', config: {} }, // pass a policy implementation directly (policyContext, config, { strapi }) => { return true; }, ] } } }); ``` ```js title="./src/api/restaurant/routes/custom-restaurant.js" module.exports = { routes: [ { method: 'GET', path: '/articles/customRoute', handler: 'api::api-name.controllerName.functionName', // or 'plugin::plugin-name.controllerName.functionName' for a plugin-specific controller config: { policies: [ // point to a registered policy 'policy-name', // point to a registered policy with some custom configuration { name: 'policy-name', config: {} }, // pass a policy implementation directly (policyContext, config, { strapi }) => { return true; }, ] }, }, ], }; ``` ```js title="./src/api/restaurant/routes/custom-restaurant.ts" routes: [ { method: 'GET', path: '/articles/customRoute', handler: 'api::api-name.controllerName.functionName', // or 'plugin::plugin-name.controllerName.functionName' for a plugin-specific controller config: { policies: [ // point to a registered policy 'policy-name', // point to a registered policy with some custom configuration { name: 'policy-name', config: {} }, // pass a policy implementation directly (policyContext, config, { strapi }) => { return true; }, ] }, }, ], }; ``` ### Middlewares [Middlewares](/cms/backend-customization/middlewares) can be added to a route configuration: - by pointing to a middleware registered in `./src/middlewares`, with or without passing a custom configuration - or by declaring the middleware implementation directly, as a function that takes [Koa](https://koajs.com/#context) (`ctx`) and the `strapi` instance as arguments: ```js title="./src/api/restaurant/routes/restaurant.js" const { createCoreRouter } = require('@strapi/strapi').factories; module.exports = createCoreRouter('api::restaurant.restaurant', { config: { find: { middlewares: [ // point to a registered middleware 'middleware-name', // point to a registered middleware with some custom configuration { name: 'middleware-name', config: {} }, // pass a middleware implementation directly (ctx, next) => { return next(); }, ] } } }); ``` ```js title="./src/api/restaurant/routes/restaurant.ts" config: { find: { middlewares: [ // point to a registered middleware 'middleware-name', // point to a registered middleware with some custom configuration { name: 'middleware-name', config: {} }, // pass a middleware implementation directly (ctx, next) => { return next(); }, ] } } }); ``` ```js title="./src/api/restaurant/routes/custom-restaurant.js" module.exports = { routes: [ { method: 'GET', path: '/articles/customRoute', handler: 'api::api-name.controllerName.functionName', // or 'plugin::plugin-name.controllerName.functionName' for a plugin-specific controller config: { middlewares: [ // point to a registered middleware 'middleware-name', // point to a registered middleware with some custom configuration { name: 'middleware-name', config: {} }, // pass a middleware implementation directly (ctx, next) => { return next(); }, ], }, }, ], }; ``` ```js title="./src/api/restaurant/routes/custom-restaurant.ts" routes: [ { method: 'GET', path: '/articles/customRoute', handler: 'api::api-name.controllerName.functionName', // or 'plugin::plugin-name.controllerName.functionName' for a plugin-specific controller config: { middlewares: [ // point to a registered middleware 'middleware-name', // point to a registered middleware with some custom configuration { name: 'middleware-name', config: {} }, // pass a middleware implementation directly (ctx, next) => { return next(); }, ], }, }, ], }; ```
Applying middlewares to all core routes of a content-type `createCoreRouter` attaches `middlewares` per action. To run the same middleware on every default Content API action (`find`, `findOne`, `create`, `update`, `delete`), list it under each key in `config`, or build the object once: ```js title="./src/api/restaurant/routes/restaurant.js" const { createCoreRouter } = require('@strapi/strapi').factories; const audit = ['global::audit-log']; const actions = ['find', 'findOne', 'create', 'update', 'delete']; module.exports = createCoreRouter('api::restaurant.restaurant', { config: Object.fromEntries(actions.map((action) => [action, { middlewares: audit }])), }); ``` ```ts title="./src/api/restaurant/routes/restaurant.ts" const audit = ['global::audit-log'] as const; const actions = ['find', 'findOne', 'create', 'update', 'delete'] as const; config: Object.fromEntries(actions.map((action) => [action, { middlewares: [...audit] }])), }); ``` If you use `only` or `except`, build the `actions` array to match the routes you actually register so you do not attach middlewares to disabled handlers.
:::tip Centralizing population logic Route-level middlewares are the recommended place to enforce consistent population rules across your API. This prevents accidental over-fetching and ensures predictable response sizes. See [Building High-Performance Strapi Applications](https://strapi.io/blog/building-high-performance-strapi-applications-common-pitfalls-and-best-practices) on the Strapi blog. ::: ### Public routes By default, routes are protected by Strapi's authentication system, which is based on [API tokens](/cms/features/api-tokens) or on the use of the [Users & Permissions plugin](/cms/features/users-permissions). In some scenarios, it can be useful to have a route publicly available and control the access outside of the normal Strapi authentication system. This can be achieved by setting the `auth` configuration parameter of a route to `false`: ```js title="./src/api/restaurant/routes/restaurant.js" const { createCoreRouter } = require('@strapi/strapi').factories; module.exports = createCoreRouter('api::restaurant.restaurant', { config: { find: { auth: false } } }); ``` ```js title="./src/api/restaurant/routes/restaurant.ts" config: { find: { auth: false } } }); ``` ```js title="./src/api/restaurant/routes/custom-restaurant.js" module.exports = { routes: [ { method: 'GET', path: '/articles/customRoute', handler: 'api::api-name.controllerName.functionName', // or 'plugin::plugin-name.controllerName.functionName' for a plugin-specific controller config: { auth: false, }, }, ], }; ``` ```js title="./src/api/restaurant/routes/custom-restaurant.ts" routes: [ { method: 'GET', path: '/articles/customRoute', handler: 'api::api-name.controllerName.functionName', // or 'plugin::plugin-name.controllerName.functionName' for a plugin-specific controller config: { auth: false, }, }, ], }; ``` ## Custom Content API parameters {#custom-content-api-parameters} You can extend the `query` and body parameters allowed on Content API routes by registering them in the [register](/cms/configurations/functions#register) lifecycle. Registered parameters are then validated and sanitized like core parameters. Clients can send extra query keys (e.g. `?search=...`) or root-level body keys (e.g. `clientMutationId`) without requiring custom routes or controllers. | What | Where | |------|--------| | Enable strict parameters (reject unknown query/body keys) | [API configuration](/cms/configurations/api): set `rest.strictParams: true` in `./config/api.js` (or `./config/api.ts`). | | Add allowed parameters (app) | Call `addQueryParams` / `addInputParams` in [register](/cms/configurations/functions#register) in `./src/index.js` or `./src/index.ts`. | | Add allowed parameters (plugin) | Call `addQueryParams` / `addInputParams` in the plugin's [register](/cms/plugins-development/server-lifecycle#register) lifecycle. | When `rest.strictParams` is enabled, only core parameters and parameters on each route's request schema are accepted; the parameters you register are merged into that schema. Use the `z` instance from `@strapi/utils` (or `zod/v4`) for schemas. ### `addQueryParams` `strapi.contentAPI.addQueryParams(options)` registers extra `query` parameters. Schemas must be scalar or array-of-scalars (string, number, boolean, enum). For nested structures, use `addInputParams` instead. Each entry can have an optional `matchRoute: (route) => boolean` callback to add the parameter only to routes for which the callback returns true. You cannot register core query param names (e.g. `filters`, `sort`, `fields`) as extra params; they are reserved. ### `addInputParams` `strapi.contentAPI.addInputParams(options)` registers extra input parameters: root-level keys in the request body (e.g. alongside `data`), with any Zod type. The optional `matchRoute` callback works the same way as for `addQueryParams`. You cannot register reserved names such as `id` or `documentId` as input params. ### `matchRoute` The `matchRoute` callback receives a `route` object with the following properties: - `route.method`: the HTTP method (`'GET'`, `'POST'`, etc.) - `route.path`: the route path - `route.handler`: the controller action string - `route.info`: metadata about the route For example, to target only GET routes, use `matchRoute: (route) => route.method === 'GET'`. To target only routes whose path includes `articles`, use `matchRoute: (route) => route.path.includes('articles')`. ```js title="./src/index.js" module.exports = { register({ strapi }) { strapi.contentAPI.addQueryParams({ search: { schema: (z) => z.string().max(200).optional(), matchRoute: (route) => route.path.includes('articles'), }, }); strapi.contentAPI.addInputParams({ clientMutationId: { schema: (z) => z.string().max(100).optional(), }, }); }, }; ``` ```ts title="./src/index.ts" register({ strapi }) { strapi.contentAPI.addQueryParams({ search: { schema: (z) => z.string().max(200).optional(), matchRoute: (route) => route.path.includes('articles'), }, }); strapi.contentAPI.addInputParams({ clientMutationId: { schema: (z) => z.string().max(100).optional(), }, }); }, }; ``` # Services Source: https://docs.strapi.io/cms/backend-customization/services # Services Services store reusable functions to keep controllers concise and follow DRY principles. This documentation explains generating or extending services with `createCoreService` and organizing them for APIs or plugins. Services are a set of reusable functions. They are particularly useful to respect the "don’t repeat yourself" (DRY) programming concept and to simplify [controllers](/cms/backend-customization/controllers.md) logic.
Simplified Strapi backend diagram with services highlighted
The diagram represents a simplified version of how a request travels through the Strapi back end, with services highlighted. The backend customization introduction page includes a complete, interactive diagram.
## Implementation Services can be [generated or added manually](#adding-a-new-service). Strapi provides a `createCoreService` factory function that automatically generates core services and allows building custom ones or [extend or replace the generated services](#extending-core-services). ### Adding a new service A new service can be implemented: - with the [interactive CLI command `strapi generate`](/cms/cli#strapi-generate) - or manually by creating a JavaScript file in the appropriate folder (see [project structure](/cms/project-structure.md)): - `./src/api/[api-name]/services/` for API services - or `./src/plugins/[plugin-name]/services/` for [plugin services](/cms/plugins-development/server-controllers-services). To manually create a service, export a factory function that returns the service implementation (i.e. an object with methods). This factory function receives the `strapi` instance: ```js title="./src/api/restaurant/services/restaurant.js" const { createCoreService } = require('@strapi/strapi').factories; module.exports = createCoreService('api::restaurant.restaurant', ({ strapi }) => ({ // Method 1: Creating an entirely new custom service async exampleService(...args) { let response = { okay: true } if (response.okay === false) { return { response, error: true } } return response }, // Method 2: Wrapping a core service (leaves core logic in place) async find(...args) { // Calling the default core controller const { results, pagination } = await super.find(...args); // some custom logic results.forEach(result => { result.counter = 1; }); return { results, pagination }; }, // Method 3: Replacing a core service async findOne(documentId, params = {}) { return strapi.documents('api::restaurant.restaurant').findOne({ documentId, // Use super to keep core fetch parameter formatting ...super.getFetchParams(params), }); } })); ``` ```ts title="./src/api/restaurant/services/restaurant.ts" // Method 1: Creating an entirely custom service async exampleService(...args) { let response = { okay: true } if (response.okay === false) { return { response, error: true } } return response }, // Method 2: Wrapping a core service (leaves core logic in place) async find(...args) { // Calling the default core controller const { results, pagination } = await super.find(...args); // some custom logic results.forEach(result => { result.counter = 1; }); return { results, pagination }; }, // Method 3: Replacing a core service async findOne(documentId, params = {}) { return strapi.documents('api::restaurant.restaurant').findOne({ documentId, // Use super to keep core fetch parameter formatting ...super.getFetchParams(params) }) as any; } })); ``` :::strapi Document Service API To get started creating your own services, see Strapi's built-in functions in the [Document Service API](/cms/api/document-service) documentation. :::
Example of a custom email service (using Nodemailer) The goal of a service is to store reusable functions. A `sendNewsletter` service could be useful to send emails from different functions in our codebase that have a specific purpose: ```js title="./src/api/restaurant/services/restaurant.js" const { createCoreService } = require('@strapi/strapi').factories; const nodemailer = require('nodemailer'); // Requires nodemailer to be installed (npm install nodemailer) // Create reusable transporter object using SMTP transport. const transporter = nodemailer.createTransport({ service: 'Gmail', auth: { user: 'user@gmail.com', pass: 'password', }, }); module.exports = createCoreService('api::restaurant.restaurant', ({ strapi }) => ({ sendNewsletter(from, to, subject, text) { // Setup e-mail data. const options = { from, to, subject, text, }; // Return a promise of the function that sends the email. return transporter.sendMail(options); }, })); ``` ```ts title="./src/api/restaurant/services/restaurant.ts" const nodemailer = require('nodemailer'); // Requires nodemailer to be installed (npm install nodemailer) // Create reusable transporter object using SMTP transport. const transporter = nodemailer.createTransport({ service: 'Gmail', auth: { user: 'user@gmail.com', pass: 'password', }, }); sendNewsletter(from, to, subject, text) { // Setup e-mail data. const options = { from, to, subject, text, }; // Return a promise of the function that sends the email. return transporter.sendMail(options); }, })); ``` The service is now available through the `strapi.service('api::restaurant.restaurant').sendNewsletter(...args)` global variable. It can be used in another part of the codebase, like in the following controller: ```js title="./src/api/restaurant/controllers/restaurant.js" module.exports = createCoreController('api::restaurant.restaurant', ({ strapi }) => ({ // GET /hello async signup(ctx) { const { userData } = ctx.body; // Store the new user in database. const user = await strapi.service('plugin::users-permissions.user').add(userData); // Send an email to validate his subscriptions. strapi.service('api::restaurant.restaurant').sendNewsletter('welcome@mysite.com', user.email, 'Welcome', '...'); // Send response to the server. ctx.send({ ok: true, }); }, })); ``` ```js title="./src/api/restaurant/controllers/restaurant.ts" // GET /hello async signup(ctx) { const { userData } = ctx.body; // Store the new user in database. const user = await strapi.service('plugin::users-permissions.user').add(userData); // Send an email to validate his subscriptions. strapi.service('api::restaurant.restaurant').sendNewsletter('welcome@mysite.com', user.email, 'Welcome', '...'); // Send response to the server. ctx.send({ ok: true, }); }, })); ```
:::note When a new [content-type](/cms/backend-customization/models.md#content-types) is created, Strapi builds a generic service with placeholder code, ready to be customized. ::: ### Extending core services Core services are created for each content-type and could be used by [controllers](/cms/backend-customization/controllers.md) to execute reusable logic through a Strapi project. Core services can be customized to implement your own logic. The following code examples should help you get started. :::tip A core service can be replaced entirely by [creating a custom service](#adding-a-new-service) and naming it the same as the core service (e.g. `find`, `findOne`, `create`, `update`, or `delete`). :::
Collection type examples ```js async find(params) { // some logic here const { results, pagination } = await super.find(params); // some more logic return { results, pagination }; } ``` ```js async findOne(documentId, params) { // some logic here const result = await super.findOne(documentId, params); // some more logic return result; } ``` ```js async create(params) { // some logic here const result = await super.create(params); // some more logic return result; } ``` ```js async update(documentId, params) { // some logic here const result = await super.update(documentId, params); // some more logic return result; } ``` ```js async delete(documentId, params) { // some logic here const result = await super.delete(documentId, params); // some more logic return result; } ```
Single type examples ```js async find(params) { // some logic here const document = await super.find(params); // some more logic return document; } ``` ```js async createOrUpdate({ data, ...params }) { // some logic here const document = await super.createOrUpdate({ data, ...params }); // some more logic return document; } ``` ```js async delete(params) { // some logic here const document = await super.delete(params); // some more logic return document; } ```
## Usage Once a service is created, it's accessible from [controllers](/cms/backend-customization/controllers.md) or from other services: ```js // access an API service strapi.service('api::apiName.serviceName').FunctionName(); // access a plugin service strapi.service('plugin::pluginName.serviceName').FunctionName(); ``` In the syntax examples above, `serviceName` is the name of the service file for API services or the name used to export the service file to `services/index.js` for plugin services. :::tip To list all the available services, run `yarn strapi services:list`. ::: ### Core service methods Services generated with `createCoreService` inherit methods that wrap the [Document Service API](/cms/api/document-service). The available methods depend on the content-type: #### Collection types | Method | Description | | --- | --- | | `find(params)` | Wrapper for [`findMany`](/cms/api/document-service#findmany); returns a paginated list of documents. | | `findOne(documentId, params)` | Wrapper for [`findOne`](/cms/api/document-service#findone); returns a single document by its `documentId`. | | `create(params)` | Wrapper for [`create`](/cms/api/document-service#create); creates a new document. | | `update(documentId, params)` | Wrapper for [`update`](/cms/api/document-service#update); updates an existing document. | | `delete(documentId, params)` | Wrapper for [`delete`](/cms/api/document-service#delete); removes a document. | | `count(params)` | Wrapper for [`count`](/cms/api/document-service#count); returns the number of matching documents. | | `publish(documentId, params)` | Wrapper for [`publish`](/cms/api/document-service#publish); publishes a draft document. | | `unpublish(documentId, params)` | Wrapper for [`unpublish`](/cms/api/document-service#unpublish); unpublishes a document. | | `discardDraft(documentId, params)` | Wrapper for [`discardDraft`](/cms/api/document-service#discarddraft); deletes the draft copy. | #### Single types | Method | Description | | --- | --- | | `find(params)` | Returns the single document (uses [`findFirst`](/cms/api/document-service#findfirst) internally). | | `createOrUpdate({ data, ...params })` | Creates the document if it doesn't exist or updates it (uses [`update`](/cms/api/document-service#update)). | | `delete(params)` | Deletes the document (uses [`delete`](/cms/api/document-service#delete)). | | `count(params)` | Counts documents matching the filters (uses [`count`](/cms/api/document-service#count)). | | `publish(params)` | Publishes a draft document (uses [`publish`](/cms/api/document-service#publish)). | | `unpublish(params)` | Unpublishes the document (uses [`unpublish`](/cms/api/document-service#unpublish)). | | `discardDraft(params)` | Deletes the draft copy (uses [`discardDraft`](/cms/api/document-service#discarddraft)). | #### Parameters and default behavior Core service methods accept the same parameters as their underlying [Document Service API](/cms/api/document-service) calls, such as `fields`, `filters`, `sort`, `pagination`, `populate`, `locale`, and `status`. When no `status` is provided, Strapi automatically sets `status: 'published'` so only published content is returned. To query draft documents, explicitly pass `status: 'draft'` or another value supported by the Document Service. The `createCoreService` factory also exposes a `getFetchParams(params)` helper that converts a controller's query object into the parameter format expected by these methods. This helper can be reused when overriding core methods to forward sanitized parameters to `strapi.documents()`. # Webhooks Source: https://docs.strapi.io/cms/backend-customization/webhooks # Webhooks Webhooks let Strapi notify external systems when content changes, while omitting the Users type for privacy. Configuration in `config/server` sets default headers and endpoints to trigger third-party processing. Webhook is a construct used by an application to notify other applications that an event occurred. More precisely, webhook is a user-defined HTTP callback. Using a webhook is a good way to tell third-party providers to start some processing (CI, build, deployment ...). The way a webhook works is by delivering information to a receiving application through HTTP requests (typically POST requests). ## User content-type webhooks To prevent from unintentionally sending any user's information to other applications, Webhooks will not work for the User content-type. If you need to notify other applications about changes in the Users collection, you can do so by creating [Lifecycle hooks](/cms/backend-customization/models#lifecycle-hooks) using the `./src/index.js` example. ## Available configurations You can set webhook configurations inside the file `./config/server`. - `webhooks` - `defaultHeaders`: You can set default headers to use for your webhook requests. This option is overwritten by the headers set in the webhook itself. **Example configuration** ```js title="./config/server.js" module.exports = { webhooks: { defaultHeaders: { "Custom-Header": "my-custom-header", }, }, }; ``` ```js title="./config/server.ts" webhooks: { defaultHeaders: { "Custom-Header": "my-custom-header", }, }, }; ``` ## Webhooks security Most of the time, webhooks make requests to public URLs, therefore it is possible that someone may find that URL and send it wrong information. To prevent this from happening you can send a header with an authentication token. Using the Admin panel you would have to do it for every webhook. Another way is to define `defaultHeaders` to add to every webhook request. You can configure these global headers by updating the file at `./config/server`: ```js title="./config/server.js" module.exports = { webhooks: { defaultHeaders: { Authorization: "Bearer my-very-secured-token", }, }, }; ``` ```js title="./config/server.ts" webhooks: { defaultHeaders: { Authorization: "Bearer my-very-secured-token", }, }, }; ``` ```js title="./config/server.js" module.exports = { webhooks: { defaultHeaders: { Authorization: `Bearer ${process.env.WEBHOOK_TOKEN}`, }, }, }; ``` ```js title="./config/server.ts" webhooks: { defaultHeaders: { Authorization: `Bearer ${process.env.WEBHOOK_TOKEN}`, }, }, }; ``` If you are developing the webhook handler yourself you can now verify the token by reading the headers. ### Verifying signatures In addition to auth headers, it's recommended to sign webhook payloads and verify signatures server‑side to prevent tampering and replay attacks. To do so, you can use the following guidelines: - Generate a shared secret and store it in environment variables - Have the sender compute an HMAC (e.g., SHA‑256) over the raw request body plus a timestamp - Send the signature (and timestamp) in headers (e.g., `X‑Webhook‑Signature`, `X‑Webhook‑Timestamp`) - On receipt, recompute the HMAC and compare using a constant‑time check - Reject if the signature is invalid or the timestamp is too old to mitigate replay
Example: Verify HMAC signatures (Node.js) Here is a minimal Node.js middleware example (pseudo‑code) showing [HMAC](https://nodejs.org/api/crypto.html#class-hmac) verification: ```js title="/src/middlewares/verify-webhook.js" const crypto = require("crypto"); module.exports = (config, { strapi }) => { const secret = process.env.WEBHOOK_SECRET; return async (ctx, next) => { const signature = ctx.get("X-Webhook-Signature"); const timestamp = ctx.get("X-Webhook-Timestamp"); if (!signature || !timestamp) return ctx.unauthorized("Missing signature"); // Compute HMAC over raw body + timestamp const raw = ctx.request.rawBody || (ctx.request.body and JSON.stringify(ctx.request.body)) || ""; const hmac = crypto.createHmac("sha256", secret); hmac.update(timestamp + "." + raw); const expected = "sha256=" + hmac.digest("hex"); // Constant-time compare + basic replay protection const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); const skew = Math.abs(Date.now() - Number(timestamp)); if (!ok or skew > 5 * 60 * 1000) { return ctx.unauthorized("Invalid or expired signature"); } await next(); }; }; ``` ```ts title="/src/middlewares/verify-webhook.ts" const secret = process.env.WEBHOOK_SECRET as string; return async (ctx: any, next: any) => { const signature = ctx.get("X-Webhook-Signature") as string; const timestamp = ctx.get("X-Webhook-Timestamp") as string; if (!signature || !timestamp) return ctx.unauthorized("Missing signature"); // Compute HMAC over raw body + timestamp const raw: string = ctx.request.rawBody || (ctx.request.body && JSON.stringify(ctx.request.body)) || ""; const hmac = crypto.createHmac("sha256", secret); hmac.update(`${timestamp}.${raw}`); const expected = `sha256=${hmac.digest("hex")}`; // Constant-time compare + basic replay protection const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); const skew = Math.abs(Date.now() - Number(timestamp)); if (!ok || skew > 5 * 60 * 1000) { return ctx.unauthorized("Invalid or expired signature"); } await next(); }; }; ``` Here are a few additional external examples: - [GitHub — Validating webhook deliveries](https://docs.github.com/webhooks/using-webhooks/validating-webhook-deliveries) - [Stripe — Verify webhook signatures](https://stripe.com/docs/webhooks/signatures)
## Available events By default Strapi webhooks can be triggered by the following events: | Name | Description | | ----------------- | ----------------------------------------------------- | | [`entry.create`](#entrycreate) | Triggered when a Content Type entry is created. | | [`entry.update`](#entryupdate) | Triggered when a Content Type entry is updated. | | [`entry.delete`](#entrydelete) | Triggered when a Content Type entry is deleted. | | [`entry.publish`](#entrypublish) | Triggered when a Content Type entry is published.\* | | [`entry.unpublish`](#entryunpublish) | Triggered when a Content Type entry is unpublished.\* | | [`media.create`](#mediacreate) | Triggered when a media is created. | | [`media.update`](#mediaupdate) | Triggered when a media is updated. | | [`media.delete`](#mediadelete) | Triggered when a media is deleted. | | [`review-workflows.updateEntryStage`](#review-workflowsupdateentrystage) | Triggered when content is moved between review stages (see [review workflows](/cms/features/review-workflows#configuration)).
This event is only available with the edition of Strapi. | | [`releases.publish`](#releases-publish) | Triggered when a Release is published (see [Releases](/cms/features/releases)).
This event is only available with the or plan of Strapi CMS. | \*only when `draftAndPublish` is enabled on this Content Type. ## Payloads :::info Private fields are not sent in the payload. ::: ### Headers When a payload is delivered to your webhook's URL, it will contain specific headers: | Header | Description | | ---------------- | ------------------------------------------ | | `X-Strapi-Event` | Name of the event type that was triggered. | ### `entry.create` This event is triggered when a new entry is created. **Example payload** ```json { "event": "entry.create", "createdAt": "2020-01-10T08:47:36.649Z", "model": "address", "entry": { "id": 1, "geolocation": {}, "city": "Paris", "postal_code": null, "category": null, "full_name": "Paris", "createdAt": "2020-01-10T08:47:36.264Z", "updatedAt": "2020-01-10T08:47:36.264Z", "cover": null, "images": [] } } ``` ### `entry.update` This event is triggered when an entry is updated. **Example payload** ```json { "event": "entry.update", "createdAt": "2020-01-10T08:58:26.563Z", "model": "address", "entry": { "id": 1, "geolocation": {}, "city": "Paris", "postal_code": null, "category": null, "full_name": "Paris", "createdAt": "2020-01-10T08:47:36.264Z", "updatedAt": "2020-01-10T08:58:26.210Z", "cover": null, "images": [] } } ``` ### `entry.delete` This event is triggered when an entry is deleted. **Example payload** ```json { "event": "entry.delete", "createdAt": "2020-01-10T08:59:35.796Z", "model": "address", "entry": { "id": 1, "geolocation": {}, "city": "Paris", "postal_code": null, "category": null, "full_name": "Paris", "createdAt": "2020-01-10T08:47:36.264Z", "updatedAt": "2020-01-10T08:58:26.210Z", "cover": null, "images": [] } } ``` ### `entry.publish` This event is triggered when an entry is published. **Example payload** ```json { "event": "entry.publish", "createdAt": "2020-01-10T08:59:35.796Z", "model": "address", "entry": { "id": 1, "geolocation": {}, "city": "Paris", "postal_code": null, "category": null, "full_name": "Paris", "createdAt": "2020-01-10T08:47:36.264Z", "updatedAt": "2020-01-10T08:58:26.210Z", "publishedAt": "2020-08-29T14:20:12.134Z", "cover": null, "images": [] } } ``` ### `entry.unpublish` This event is triggered when an entry is unpublished. **Example payload** ```json { "event": "entry.unpublish", "createdAt": "2020-01-10T08:59:35.796Z", "model": "address", "entry": { "id": 1, "geolocation": {}, "city": "Paris", "postal_code": null, "category": null, "full_name": "Paris", "createdAt": "2020-01-10T08:47:36.264Z", "updatedAt": "2020-01-10T08:58:26.210Z", "publishedAt": null, "cover": null, "images": [] } } ``` ### `media.create` This event is triggered when you upload a file on entry creation or through the media interface. **Example payload** ```json { "event": "media.create", "createdAt": "2020-01-10T10:58:41.115Z", "media": { "id": 1, "name": "image.png", "hash": "353fc98a19e44da9acf61d71b11895f9", "sha256": "huGUaFJhmcZRHLcxeQNKblh53vtSUXYaB16WSOe0Bdc", "ext": ".png", "mime": "image/png", "size": 228.19, "url": "/uploads/353fc98a19e44da9acf61d71b11895f9.png", "provider": "local", "provider_metadata": null, "createdAt": "2020-01-10T10:58:41.095Z", "updatedAt": "2020-01-10T10:58:41.095Z", "related": [] } } ``` ### `media.update` This event is triggered when you replace a media or update the metadata of a media through the media interface. **Example payload** ```json { "event": "media.update", "createdAt": "2020-01-10T10:58:41.115Z", "media": { "id": 1, "name": "image.png", "hash": "353fc98a19e44da9acf61d71b11895f9", "sha256": "huGUaFJhmcZRHLcxeQNKblh53vtSUXYaB16WSOe0Bdc", "ext": ".png", "mime": "image/png", "size": 228.19, "url": "/uploads/353fc98a19e44da9acf61d71b11895f9.png", "provider": "local", "provider_metadata": null, "createdAt": "2020-01-10T10:58:41.095Z", "updatedAt": "2020-01-10T10:58:41.095Z", "related": [] } } ``` ### `media.delete` This event is triggered only when you delete a media through the media interface. **Example payload** ```json { "event": "media.delete", "createdAt": "2020-01-10T11:02:46.232Z", "media": { "id": 11, "name": "photo.png", "hash": "43761478513a4c47a5fd4a03178cfccb", "sha256": "HrpDOKLFoSocilA6B0_icA9XXTSPR9heekt2SsHTZZE", "ext": ".png", "mime": "image/png", "size": 4947.76, "url": "/uploads/43761478513a4c47a5fd4a03178cfccb.png", "provider": "local", "provider_metadata": null, "createdAt": "2020-01-07T19:34:32.168Z", "updatedAt": "2020-01-07T19:34:32.168Z", "related": [] } } ``` ### `review-workflows.updateEntryStage` This event is only available with the plan of Strapi.
The event is triggered when content is moved to a new review stage (see [Review Workflows](/cms/features/review-workflows#configuration)). **Example payload** ```json { "event": "review-workflows.updateEntryStage", "createdAt": "2023-06-26T15:46:35.664Z", "model": "model", "uid": "uid", "entity": { "id": 2 }, "workflow": { "id": 1, "stages": { "from": { "id": 1, "name": "Stage 1" }, "to": { "id": 2, "name": "Stage 2" } } } } ``` ### `releases.publish` {#releases-publish} The event is triggered when a [release](/cms/features/releases) is published. **Example payload** ```json { "event": "releases.publish", "createdAt": "2024-02-21T16:45:36.877Z", "isPublished": true, "release": { "id": 2, "name": "Fall Winter highlights", "releasedAt": "2024-02-21T16:45:36.873Z", "scheduledAt": null, "timezone": null, "createdAt": "2024-02-21T15:16:22.555Z", "updatedAt": "2024-02-21T16:45:36.875Z", "actions": { "count": 1 } } } ``` ## Best practices for webhook handling - Validate incoming requests by checking headers and payload signatures. - Implement retries for failed webhook requests to handle transient errors. - Log webhook events for debugging and monitoring. - Use secure, HTTPS endpoints for receiving webhooks. - Set up rate limiting to avoid being overwhelmed by multiple webhook requests. :::tip If you want to learn more about how to use webhooks with Next.js, please have a look at the [dedicated blog article](https://strapi.io/blog/how-to-create-an-ssg-static-site-generation-application-with-strapi-webhooks-and-nextjs). ::: # Billing portal Source: https://docs.strapi.io/cms/billing-portal # Billing portal The Strapi billing portal is where you can view all Strapi subscriptions and manage payment methods, billing details, and invoices. Only Growth subscriptions can be managed in the portal; Cloud and Enterprise subscriptions are view-only. The [Strapi billing portal](https://billing.strapi.io) is where you view and manage billing for your Strapi subscriptions. For all subscriptions, you can update payment methods, edit billing details, and download invoices. While you can view all subscriptions in the portal, only Growth subscriptions can be managed directly there. Cloud subscriptions are managed in the [Strapi Cloud dashboard](/cloud/projects/settings#plans). To change Enterprise contract terms, [contact sales](mailto:sales@strapi.io). ## Sign in To sign in to the [billing portal](https://billing.strapi.io): 1. Enter the email address you used at purchase or for CLI authentication. 2. Enter the 6-digit code sent to your inbox. 3. If your email is linked to multiple billing accounts, select the account you want to manage. ## Subscriptions The *Subscriptions* tab displays all your Strapi subscriptions. Subscriptions are grouped into the following sections: - **In trial**: Trial subscriptions that have not yet converted to paid. - **Active**: Active subscriptions. - **Scheduled for cancellation**: Subscriptions scheduled to be canceled at the end of the current billing period. - **Canceled**: Fully canceled subscriptions. Each subscription card shows the plan name, product family, status, price, billing period (monthly or yearly), renewal or trial end date, and subscription ID. Next to the subscription ID, Cloud subscriptions show the Cloud project name and Growth subscriptions show the CMS project ID linked to the license. ### Activating an in-trial Growth subscription Before you activate, make sure to add a payment method in the [Payment methods](#payment-methods) tab and complete your [Billing details](#billing-details) profile. To activate a subscription: 1. Click **Activate subscription** in the *In trial* section. 2. In the *Manage subscription* modal, set the desired seat count and choose if the plan should include the SSO add-on. 3. Click **Continue** to review the charge summary. 4. Click **Activate now** to confirm. :::note Growth subscriptions must include a minimum of 3 seats. ::: ### Changing seats and add-ons on a Growth subscription To update the seat count or add-ons of an active Growth subscription: 1. Click **Manage subscription**. 2. In the *Manage subscription* modal, adjust the seat count and included add-ons as needed. 3. Click **Continue** to review the charge summary. 4. Click **Confirm** to apply the changes. :::note When you add seats or enable the SSO add-on, you are charged a prorated amount immediately. However, your Strapi CMS instance checks for license updates on startup and every 12 hours, so new seats may take up to 12 hours to appear in the admin panel. To apply the change sooner, restart your Strapi instance. When you remove seats or disable the SSO add-on, the change takes effect at the next renewal. Your current seat count and add-ons stay active until then. If you add seats and disable SSO in the same change, the seat increase is charged immediately. SSO removal is still deferred to the next renewal. ::: ### Canceling a Growth subscription To cancel an active Growth subscription: 1. Click **Cancel subscription**. 2. In the *Cancel subscription* dialog, click **Confirm** to schedule the cancellation. Cancellations are effective at the end of the billing period. While the change is pending, you can undo the scheduled cancellation and reactivate your subscription. You can also reactivate a canceled subscription even after the cancellation has taken effect. To reactivate, click **Reactivate** on the subscription card and follow the steps. ## Payment methods The *Payment methods* tab lets you manage the payment cards used for your subscriptions. ### Adding a new card To add a new card: 1. Click **Add card**. 2. In the *Add payment method* dialog, enter the card number, expiration date, and CVV/CVC. 3. Optionally, tick **Set as default payment method**. 4. Click **Save**. :::note The default card will be used for all subscription-related transactions, including add-ons and overages. ::: ### Updating or removing a card To update an existing payment card: 1. Click the icon of the payment card you want to edit. 2. Do one of the following: - Click **Set as default** to make this your default card. - Click **Edit card**, update the card number, expiration date, and CVV/CVC, then click **Save**. To remove an existing payment card: 1. Click the icon of the payment card you want to delete. 2. Click **Remove card**. 3. Click **Confirm** in the *Remove payment method* dialog. :::caution You cannot remove your default card. In cases where a secondary card is attached to a subscription (e.g. for Cloud subscriptions), removing the card will automatically cancel all the subscriptions attached to that card. ::: ## Billing details The *Billing details* tab lets you view and edit account billing information. Required fields in this section must be completed to activate a trial subscription. :::note Taxes may be added to your invoice based on your billing address: - In the EU, UK, Canada, and India, providing a valid VAT ID exempts you from VAT. If no valid VAT ID is provided, VAT will be added to your invoice. - In the US, applicable sales taxes are calculated based on your state and address. ::: ## Invoices The *Invoices* tab displays all invoices for your Strapi subscriptions and their status. Invoices can have any of the following statuses: - **Paid**: the payment has been received and the invoice is available, no additional action is required. - **Pending**: the invoice is not complete or validated yet or the payment didn't go through and needs to be fixed. - **Unpaid**: the payment has failed and won't automatically be retried. - **Voided**: the invoice has been canceled. Click the ![download icon](/img/assets/icons/download.svg) icon to download an invoice. # Command Line Interface Source: https://docs.strapi.io/cms/cli # Command Line Interface (CLI) Strapi comes with a full featured Command Line Interface (CLI) which lets you scaffold and manage your project in seconds. The CLI works with both the `yarn` and `npm` package managers. Strapi CLI commands on the present page are grouped by category: | Category | Commands | |---|---| | [Development](#development) | `develop`, `start`, `build`, `console` | | [Cloud](#cloud) | `login`, `logout`, `deploy` | | [Data management](#data-management) | `export`, `import`, `transfer` | | [Project information](#project-information) | `report`, `telemetry:disable`, `telemetry:enable`, `version`, `help` | | [Configuration](#configuration) | `configuration:dump`, `configuration:restore` | | [Administration](#administration) | `admin:create-user`, `admin:reset-user-password`, `admin:list-users`, `admin:active-user`, `admin:block-user`, `admin:delete-user` | | [Code generation](#code-generation) | `generate`, `openapi generate`, `templates:generate`, `ts:generate-types` | | [Listing](#listing) | `routes:list`, `policies:list`, `middlewares:list`, `content-types:list`, `hooks:list`, `controllers:list`, `services:list` | :::caution Interactive commands such as `strapi admin:create-user` don't display prompts with `npm`. Please consider using the `yarn` package manager. ::: ## General considerations It is recommended to install Strapi locally only, which requires prefixing all of the following `strapi` commands with the package manager used for the project setup (e.g `npm run strapi help` or `yarn strapi help`) or a dedicated node package executor (e.g. `npx strapi help`). :::note The format for passing options differs between npm and yarn * To pass options with `npm` use the following syntax:
`npm run strapi -- --