Backend customization

Routing

./api/**/config/routes.json files define all available endpoints for the clients.

By default, Strapi generates endpoints for all your Content Types. More information is in the Content API documentation.

How to create a route?

You have to edit the routes.json file in one of your APIs folders (./api/**/config/routes.json) and manually add a new route object into the routes array.

Path — ./api/**/config/routes.json.

  1. {
  2. "routes": [
  3. {
  4. "method": "GET",
  5. "path": "/restaurants",
  6. "handler": "Restaurant.find",
  7. "config": {
  8. "policies": []
  9. }
  10. },
  11. {
  12. "method": "PUT",
  13. "path": "/restaurants/bulkUpdate",
  14. "handler": "Restaurant.bulkUpdate",
  15. "config": {
  16. "policies": []
  17. }
  18. },
  19. {
  20. "method": "POST",
  21. "path": "/restaurants/:id/reservation",
  22. "handler": "Restaurant.reservation",
  23. "config": {
  24. "policies": ["isAuthenticated", "hasCreditCard"]
  25. }
  26. }
  27. ]
  28. }
  • method (string): Method or array of methods to hit the route (e.g. GET, POST, PUT, HEAD, DELETE, PATCH).
  • path (string): URL starting with / (e.g. /restaurants).
  • handler (string): Action to execute when the route is hit following this syntax <Controller>.<action>.
  • config
    • policies (array): Array of policy names or paths (see more)

TIP

You can exclude the entire config object if you do not want the route to be checked by the Users & Permissions plugin.

Dynamic parameters

The router used by Strapi allows you to create dynamic routes where you can use parameters and simple regular expressions. These parameters will be exposed in the ctx.params object. For more details, please refer to the PathToRegexBackend customization - 图1 (opens new window) documentation.

  1. {
  2. "routes": [
  3. {
  4. "method": "GET",
  5. "path": "/restaurants/:category/:id",
  6. "handler": "Restaurant.findOneByCategory",
  7. "config": {
  8. "policies": []
  9. }
  10. },
  11. {
  12. "method": "GET",
  13. "path": "/restaurants/:region(\\d{2}|\\d{3})/:id", // Only match when the first parameter contains 2 or 3 digits.
  14. "handler": "Restaurant.findOneByRegion",
  15. "config": {
  16. "policies": []
  17. }
  18. }
  19. ]
  20. }

Example

Route definition with URL params

  1. {
  2. "routes": [
  3. {
  4. "method": "GET",
  5. "path": "/restaurants/:id",
  6. "handler": "Restaurant.findOne",
  7. "config": {
  8. "policies": []
  9. }
  10. }
  11. ]
  12. }

Get the URL param in the controller

  1. module.exports = {
  2. findOne: async ctx => {
  3. // const id = ctx.params.id;
  4. const { id } = ctx.params;
  5. return id;
  6. },
  7. };

Policies

Policies are functions which have the ability to execute specific logic on each request before it reaches the controller’s action. They are mostly used for securing business logic easily. Each route of the project can be associated to an array of policies. For example, you can create a policy named isAdmin, which obviously checks that the request is sent by an admin user, and use it for critical routes.

The policies are defined in each ./api/**/config/policies/ folders and plugins. They are respectively exposed through strapi.api.**.config.policies and strapi.plugins.**.config.policies. The global policies are defined at ./config/policies/ and accessible via strapi.config.policies.

How to create a policy?

There are several ways to create a policy.

  • Using the CLI strapi generate:policy isAuthenticated.
    Read the CLI documentation for more information.
  • Manually create a JavaScript file named isAuthenticated.js in ./config/policies/.

Path — ./config/policies/isAuthenticated.js.

  1. module.exports = async (ctx, next) => {
  2. if (ctx.state.user) {
  3. // Go to next policy or will reach the controller's action.
  4. return await next();
  5. }
  6. ctx.unauthorized(`You're not logged in!`);
  7. };

In this example, we are verifying that a session is open. If it is the case, we call the next() method that will execute the next policy or controller’s action. Otherwise, a 401 error is returned.

Usage

To apply policies to a route, you need to associate an array of policies to it. There are two kinds of policies: global and scoped.

WARNING

To apply policies with GraphQL please see the following guide.

Global policies

The global policies can be associated to any route in your project.

Path — ./api/restaurant/routes.json.

  1. {
  2. "routes": [
  3. {
  4. "method": "GET",
  5. "path": "/restaurants",
  6. "handler": "Restaurant.find",
  7. "config": {
  8. "policies": ["global::isAuthenticated"]
  9. }
  10. }
  11. ]
  12. }

Before executing the find action in the Restaurant.js controller, the global policy isAuthenticated located in ./config/policies/isAuthenticated.js will be called.

TIP

You can put as much policy as you want in this array. However be careful about the performance impact.

Plugins policies

Plugins can add and expose policies into your app. For example, the plugin Users & Permissions comes with useful policies to ensure that the user is well authenticated or has the rights to perform an action.

Path — ./api/restaurant/config/routes.json.

  1. {
  2. "routes": [
  3. {
  4. "method": "GET",
  5. "path": "/restaurants",
  6. "handler": "Restaurant.find",
  7. "config": {
  8. "policies": ["plugins::users-permissions.isAuthenticated"]
  9. }
  10. }
  11. ]
  12. }

The policy isAuthenticated located in the users-permissions plugin will be executed before the find action in the Restaurant.js controller.

API policies

The API policies can be associated to the routes defined in the API where they have been declared.

Path — ./api/restaurant/config/policies/isAdmin.js.

  1. module.exports = async (ctx, next) => {
  2. if (ctx.state.user.role.name === 'Administrator') {
  3. // Go to next policy or will reach the controller's action.
  4. return await next();
  5. }
  6. ctx.unauthorized(`You're not allowed to perform this action!`);
  7. };

Path — ./api/restaurant/config/routes.json.

  1. {
  2. "routes": [
  3. {
  4. "method": "GET",
  5. "path": "/restaurants",
  6. "handler": "Restaurant.find",
  7. "config": {
  8. "policies": ["isAdmin"]
  9. }
  10. }
  11. ]
  12. }

The policy isAdmin located in ./api/restaurant/config/policies/isAdmin.js will be executed before the find action in the Restaurant.js controller.

Using a policy outside its api

To use a policy in another api you can reference it with the following syntax: {apiName}.{policyName}.

Path — ./api/category/config/routes.json.

  1. {
  2. "routes": [
  3. {
  4. "method": "GET",
  5. "path": "/categories",
  6. "handler": "Category.find",
  7. "config": {
  8. "policies": ["restaurant.isAdmin"]
  9. }
  10. }
  11. ]
  12. }

Advanced usage

As it’s explained above, the policies are executed before the controller’s action. It looks like an action that you can make before the controller’s action. You can also execute a logic after.

Path — ./config/policies/custom404.js.

  1. module.exports = async (ctx, next) => {
  2. // Indicate to the server to go to
  3. // the next policy or to the controller's action.
  4. await next();
  5. // The code below will be executed after the controller's action.
  6. if (ctx.status === 404) {
  7. ctx.body = 'We cannot find the resource.';
  8. }
  9. };

Controllers

Controllers are JavaScript files which contain a set of methods called actions reached by the client according to the requested route. It means that every time a client requests the route, the action performs the business logic coded and sends back the response. They represent the C in the MVC pattern. In most cases, the controllers will contain the bulk of a project’s business logic.

  1. module.exports = {
  2. // GET /hello
  3. async index(ctx) {
  4. return 'Hello World!';
  5. },
  6. };

In this example, any time a web browser is pointed to the /hello URL on your app, the page will display the text: Hello World!.

The controllers are defined in each ./api/**/controllers/ folder. Every JavaScript file put in these folders will be loaded as a controller. They are also available through the strapi.controllers and strapi.api.**.controllers global variables.

Core controllers

When you create a new Content Type you will see a new empty controller has been created. This is because Strapi builds a generic controller for your models by default and allows you to override and extend it in the generated files.

Extending a Model Controller

Here are the core methods (and their current implementation). You can simply copy and paste this code in your own controller file to customize the methods.

WARNING

In the following example we will assume your controller, service and model are named restaurant

Utils

First require the utility functions

  1. const { parseMultipartData, sanitizeEntity } = require('strapi-utils');
  • parseMultipartData: This function parses Strapi’s formData format.
  • sanitizeEntity: This function removes all private fields from the model and its relations.
Collection Type
find
  1. const { sanitizeEntity } = require('strapi-utils');
  2. module.exports = {
  3. /**
  4. * Retrieve records.
  5. *
  6. * @return {Array}
  7. */
  8. async find(ctx) {
  9. let entities;
  10. if (ctx.query._q) {
  11. entities = await strapi.services.restaurant.search(ctx.query);
  12. } else {
  13. entities = await strapi.services.restaurant.find(ctx.query);
  14. }
  15. return entities.map(entity => sanitizeEntity(entity, { model: strapi.models.restaurant }));
  16. },
  17. };
findOne
  1. const { sanitizeEntity } = require('strapi-utils');
  2. module.exports = {
  3. /**
  4. * Retrieve a record.
  5. *
  6. * @return {Object}
  7. */
  8. async findOne(ctx) {
  9. const { id } = ctx.params;
  10. const entity = await strapi.services.restaurant.findOne({ id });
  11. return sanitizeEntity(entity, { model: strapi.models.restaurant });
  12. },
  13. };
count
  1. module.exports = {
  2. /**
  3. * Count records.
  4. *
  5. * @return {Number}
  6. */
  7. count(ctx) {
  8. if (ctx.query._q) {
  9. return strapi.services.restaurant.countSearch(ctx.query);
  10. }
  11. return strapi.services.restaurant.count(ctx.query);
  12. },
  13. };
create
  1. const { parseMultipartData, sanitizeEntity } = require('strapi-utils');
  2. module.exports = {
  3. /**
  4. * Create a record.
  5. *
  6. * @return {Object}
  7. */
  8. async create(ctx) {
  9. let entity;
  10. if (ctx.is('multipart')) {
  11. const { data, files } = parseMultipartData(ctx);
  12. entity = await strapi.services.restaurant.create(data, { files });
  13. } else {
  14. entity = await strapi.services.restaurant.create(ctx.request.body);
  15. }
  16. return sanitizeEntity(entity, { model: strapi.models.restaurant });
  17. },
  18. };
update
  1. const { parseMultipartData, sanitizeEntity } = require('strapi-utils');
  2. module.exports = {
  3. /**
  4. * Update a record.
  5. *
  6. * @return {Object}
  7. */
  8. async update(ctx) {
  9. const { id } = ctx.params;
  10. let entity;
  11. if (ctx.is('multipart')) {
  12. const { data, files } = parseMultipartData(ctx);
  13. entity = await strapi.services.restaurant.update({ id }, data, {
  14. files,
  15. });
  16. } else {
  17. entity = await strapi.services.restaurant.update({ id }, ctx.request.body);
  18. }
  19. return sanitizeEntity(entity, { model: strapi.models.restaurant });
  20. },
  21. };
delete
  1. const { sanitizeEntity } = require('strapi-utils');
  2. module.exports = {
  3. /**
  4. * Delete a record.
  5. *
  6. * @return {Object}
  7. */
  8. async delete(ctx) {
  9. const { id } = ctx.params;
  10. const entity = await strapi.services.restaurant.delete({ id });
  11. return sanitizeEntity(entity, { model: strapi.models.restaurant });
  12. },
  13. };
Single Type
find
  1. const { sanitizeEntity } = require('strapi-utils');
  2. module.exports = {
  3. /**
  4. * Retrieve the record.
  5. *
  6. * @return {Object}
  7. */
  8. async find(ctx) {
  9. const entity = await strapi.services.restaurant.find();
  10. return sanitizeEntity(entity, { model: strapi.models.restaurant });
  11. },
  12. };
update
  1. const { parseMultipartData, sanitizeEntity } = require('strapi-utils');
  2. module.exports = {
  3. /**
  4. * Update the record.
  5. *
  6. * @return {Object}
  7. */
  8. async update(ctx) {
  9. let entity;
  10. if (ctx.is('multipart')) {
  11. const { data, files } = parseMultipartData(ctx);
  12. entity = await strapi.services.restaurant.createOrUpdate(data, {
  13. files,
  14. });
  15. } else {
  16. entity = await strapi.services.restaurant.createOrUpdate(ctx.request.body);
  17. }
  18. return sanitizeEntity(entity, { model: strapi.models.restaurant });
  19. },
  20. };
delete
  1. const { sanitizeEntity } = require('strapi-utils');
  2. module.exports = {
  3. /**
  4. * Delete the record.
  5. *
  6. * @return {Object}
  7. */
  8. async delete(ctx) {
  9. const entity = await strapi.services.restaurant.delete();
  10. return sanitizeEntity(entity, { model: strapi.models.restaurant });
  11. },
  12. };

Custom controllers

You can also create custom controllers to build your own business logic and API endpoints.

There are two ways to create a controller:

  • Using the CLI strapi generate:controller restaurant.
    Read the CLI documentation for more information.
  • Manually create a JavaScript file in ./api/**/controllers.

Adding Endpoints

Each controller’s action must be an async function. Every action receives a context (ctx) object as first parameter containing the request context and the response context.

Example

In this example, we are defining a specific route in ./api/hello/config/routes.json that takes Hello.index as handler. For more information on routing, please see the Routing documentation

It means that every time a request GET /hello is sent to the server, Strapi will call the index action in the Hello.js controller. Our index action will return Hello World!. You can also return a JSON object.

Path — ./api/hello/config/routes.json.

  1. {
  2. "routes": [
  3. {
  4. "method": "GET",
  5. "path": "/hello",
  6. "handler": "Hello.index",
  7. "config": {
  8. "policies": []
  9. }
  10. }
  11. ]
  12. }

Path — ./api/hello/controllers/Hello.js.

  1. module.exports = {
  2. // GET /hello
  3. async index(ctx) {
  4. ctx.send('Hello World!');
  5. },
  6. };

TIP

A route handler can only access the controllers defined in the ./api/**/controllers folders.

Requests & Responses

Requests

The context object (ctx) contains all the requests related information. They are accessible through ctx.request, from controllers and policies.

Strapi passes the body on ctx.request.body and files through ctx.request.files

For more information, please refer to the Koa request documentationBackend customization - 图2 (opens new window).

Responses

The context object (ctx) contains a list of values and functions useful to manage server responses. They are accessible through ctx.response, from controllers and policies.

For more information, please refer to the Koa response documentationBackend customization - 图3 (opens new window).

Services

Services are a set of reusable functions. They are particularly useful to respect the DRY (don’t repeat yourself) programming concept and to simplify controllers logic.

Core services

When you create a new Content Type or a new model, you will see a new empty service has been created. It is because Strapi builds a generic service for your models by default and allows you to override and extend it in the generated files.

Extending a Model Service

Here are the core methods (and their current implementation). You can simply copy and paste this code to your own service file to customize the methods.

You can read about strapi.query calls here.

TIP

In the following example your controller, service and model are named restaurant.

Utils

If you’re extending the create or update service, first require the following utility function:

  1. const { isDraft } = require('strapi-utils').contentTypes;
  • isDraft: This function checks if the entry is a draft.
Collection Type
find
  1. module.exports = {
  2. /**
  3. * Promise to fetch all records
  4. *
  5. * @return {Promise}
  6. */
  7. find(params, populate) {
  8. return strapi.query('restaurant').find(params, populate);
  9. },
  10. };
  1. {
  2. "name": "Tokyo Sushi"
  3. }
  4. // or
  5. {
  6. "_limit": 20,
  7. "name_contains": "sushi"
  8. }
  • populate (array): you have to mention data you want populate ["author", "author.name", "comment", "comment.content"]
findOne
  1. module.exports = {
  2. /**
  3. * Promise to fetch record
  4. *
  5. * @return {Promise}
  6. */
  7. findOne(params, populate) {
  8. return strapi.query('restaurant').findOne(params, populate);
  9. },
  10. };
  1. {
  2. "name": "Tokyo Sushi"
  3. }
  4. // or
  5. {
  6. "name_contains": "sushi"
  7. }
  • populate (array): you have to mention data you want populate ["author", "author.name", "comment", "comment.content"]
count
  1. module.exports = {
  2. /**
  3. * Promise to count record
  4. *
  5. * @return {Promise}
  6. */
  7. count(params) {
  8. return strapi.query('restaurant').count(params);
  9. },
  10. };
  1. {
  2. "name": "Tokyo Sushi"
  3. }
  4. // or
  5. {
  6. "name_contains": "sushi"
  7. }
create
  1. const { isDraft } = require('strapi-utils').contentTypes;
  2. module.exports = {
  3. /**
  4. * Promise to add record
  5. *
  6. * @return {Promise}
  7. */
  8. async create(data, { files } = {}) {
  9. const isDraft = isDraft(data, strapi.models.restaurant);
  10. const validData = await strapi.entityValidator.validateEntityCreation(
  11. strapi.models.restaurant,
  12. data,
  13. { isDraft }
  14. );
  15. const entry = await strapi.query('restaurant').create(validData);
  16. if (files) {
  17. // automatically uploads the files based on the entry and the model
  18. await strapi.entityService.uploadFiles(entry, files, {
  19. model: 'restaurant',
  20. // if you are using a plugin's model you will have to add the `source` key (source: 'users-permissions')
  21. });
  22. return this.findOne({ id: entry.id });
  23. }
  24. return entry;
  25. },
  26. };
update
  1. const { isDraft } = require('strapi-utils').contentTypes;
  2. module.exports = {
  3. /**
  4. * Promise to edit record
  5. *
  6. * @return {Promise}
  7. */
  8. async update(params, data, { files } = {}) {
  9. const existingEntry = await db.query('restaurant').findOne(params);
  10. const isDraft = isDraft(existingEntry, strapi.models.restaurant);
  11. const validData = await strapi.entityValidator.validateEntityUpdate(
  12. strapi.models.restaurant,
  13. data,
  14. { isDraft }
  15. );
  16. const entry = await strapi.query('restaurant').update(params, validData);
  17. if (files) {
  18. // automatically uploads the files based on the entry and the model
  19. await strapi.entityService.uploadFiles(entry, files, {
  20. model: 'restaurant',
  21. // if you are using a plugin's model you will have to add the `source` key (source: 'users-permissions')
  22. });
  23. return this.findOne({ id: entry.id });
  24. }
  25. return entry;
  26. },
  27. };
  • params (object): it should look like this {id: 1}
delete
  1. module.exports = {
  2. /**
  3. * Promise to delete a record
  4. *
  5. * @return {Promise}
  6. */
  7. delete(params) {
  8. return strapi.query('restaurant').delete(params);
  9. },
  10. };
  • params (object): it should look like this {id: 1}
search
  1. module.exports = {
  2. /**
  3. * Promise to search records
  4. *
  5. * @return {Promise}
  6. */
  7. search(params) {
  8. return strapi.query('restaurant').search(params);
  9. },
  10. };
  1. {
  2. "name": "Tokyo Sushi"
  3. }
  4. // or
  5. {
  6. "name_contains": "sushi"
  7. }
countSearch
  1. module.exports = {
  2. /**
  3. * Promise to count searched records
  4. *
  5. * @return {Promise}
  6. */
  7. countSearch(params) {
  8. return strapi.query('restaurant').countSearch(params);
  9. },
  10. };
  1. {
  2. "name": "Tokyo Sushi"
  3. }
  4. // or
  5. {
  6. "name_contains": "sushi"
  7. }
Single Type
find
  1. const _ = require('lodash');
  2. module.exports = {
  3. /**
  4. * Promise to fetch the record
  5. *
  6. * @return {Promise}
  7. */
  8. async find(populate) {
  9. const results = await strapi.query('restaurant').find({ _limit: 1 }, populate);
  10. return _.first(results) || null;
  11. },
  12. };
  • populate (array): you have to mention data you want populate ["author", "author.name", "comment", "comment.content"]
createOrUpdate
  1. const _ = require('lodash');
  2. module.exports = {
  3. /**
  4. * Promise to add/update the record
  5. *
  6. * @return {Promise}
  7. */
  8. async createOrUpdate(data, { files } = {}) {
  9. const results = await strapi.query('restaurant').find({ _limit: 1 });
  10. const entity = _.first(results) || null;
  11. let entry;
  12. if (!entity) {
  13. entry = await strapi.query('restaurant').create(data);
  14. } else {
  15. entry = await strapi.query('restaurant').update({ id: entity.id }, data);
  16. }
  17. if (files) {
  18. // automatically uploads the files based on the entry and the model
  19. await strapi.entityService.uploadFiles(entry, files, {
  20. model: 'restaurant',
  21. // if you are using a plugin's model you will have to add the `plugin` key (plugin: 'users-permissions')
  22. });
  23. return this.findOne({ id: entry.id });
  24. }
  25. return entry;
  26. },
  27. };
delete
  1. module.exports = {
  2. /**
  3. * Promise to delete a record
  4. *
  5. * @return {Promise}
  6. */
  7. delete() {
  8. const results = await strapi.query('restaurant').find({ _limit: 1 });
  9. const entity = _.first(results) || null;
  10. if (!entity) return;
  11. return strapi.query('restaurant').delete({id: entity.id});
  12. },
  13. };

Custom services

You can also create custom services to build your own business logic.

There are two ways to create a service.

  • Using the CLI strapi generate:service restaurant.
    Read the CLI documentation for more information.
  • Manually create a JavaScript file named in ./api/**/services/.

Example

The goal of a service is to store reusable functions. An email service could be useful to send emails from different functions in our codebase:

Path — ./api/email/services/Email.js.

  1. const nodemailer = require('nodemailer');
  2. // Create reusable transporter object using SMTP transport.
  3. const transporter = nodemailer.createTransport({
  4. service: 'Gmail',
  5. auth: {
  6. user: 'user@gmail.com',
  7. pass: 'password',
  8. },
  9. });
  10. module.exports = {
  11. send: (from, to, subject, text) => {
  12. // Setup e-mail data.
  13. const options = {
  14. from,
  15. to,
  16. subject,
  17. text,
  18. };
  19. // Return a promise of the function that sends the email.
  20. return transporter.sendMail(options);
  21. },
  22. };

TIP

please make sure you installed nodemailer (npm install nodemailer) for this example.

The service is now available through the strapi.services global variable. We can use it in another part of our codebase. For example a controller like below:

Path — ./api/user/controllers/User.js.

  1. module.exports = {
  2. // GET /hello
  3. signup: async ctx => {
  4. // Store the new user in database.
  5. const user = await User.create(ctx.query);
  6. // Send an email to validate his subscriptions.
  7. strapi.services.email.send('welcome@mysite.com', user.email, 'Welcome', '...');
  8. // Send response to the server.
  9. ctx.send({
  10. ok: true,
  11. });
  12. },
  13. };

Queries

Strapi provides a utility function strapi.query to make database queries.

You can just call strapi.query('modelName', 'pluginName') to access the query API for any model.

These queries handle for you specific Strapi features like components, dynamic zones, filters and search.

API Reference

find

This method returns a list of entries matching Strapi filters. You can also pass a populate option to specify which relations you want to be populated.

Examples

Find by id:

  1. strapi.query('restaurant').find({ id: 1 });

Find by in IN, with a limit:

  1. strapi.query('restaurant').find({ _limit: 10, id_in: [1, 2] });

Find by date orderBy name:

  1. strapi.query('restaurant').find({ date_gt: '2019-01-01T00:00:00Z', _sort: 'name:desc' });

Find by id not in and populate a relation. Skip the first ten results

  1. strapi.query('restaurant').find({ id_nin: [1], _start: 10 }, ['category', 'category.name']);

findOne

This method returns the first entry matching some basic params. You can also pass a populate option to specify which relations you want to be populated.

Examples

Find one by id:

  1. strapi.query('restaurant').findOne({ id: 1 });

Find one by name:

  1. strapi.query('restaurant').findOne({ name: 'restaurant name' });

Find one by name and creation_date:

  1. strapi.query('restaurant').findOne({ name: 'restaurant name', date: '2019-01-01T00:00:00Z' });

Find one by id and populate a relation

  1. strapi.query('restaurant').findOne({ id: 1 }, ['category', 'category.name']);

create

Creates an entry in the database and returns the entry.

Example
  1. strapi.query('restaurant').create({
  2. name: 'restaurant name',
  3. // this is a dynamiczone field. the order is persisted in db.
  4. content: [
  5. {
  6. __component: 'blog.rich-text',
  7. title: 'Some title',
  8. subTitle: 'Some sub title',
  9. },
  10. {
  11. __component: 'blog.quote',
  12. quote: 'Some interesting quote',
  13. author: 1,
  14. },
  15. ],
  16. // this is a component field. the order is persisted in db.
  17. opening_hours: [
  18. {
  19. day_interval: 'Mon',
  20. opening_hour: '7:00 PM',
  21. closing_hour: '11:00 PM',
  22. },
  23. {
  24. day_interval: 'Tue',
  25. opening_hour: '7:00 PM',
  26. closing_hour: '11:00 PM',
  27. },
  28. ],
  29. // pass the id of a media to link it to the entry
  30. cover: 1,
  31. // automatically creates the relations when passing the ids in the field
  32. reviews: [1, 2, 3],
  33. });

update

Updates an entry in the database and returns the entry.

Examples

Update by id

  1. strapi.query('restaurant').update(
  2. { id: 1 },
  3. {
  4. name: 'restaurant name',
  5. content: [
  6. {
  7. __component: 'blog.rich-text',
  8. title: 'Some title',
  9. subTitle: 'Some sub title',
  10. },
  11. {
  12. __component: 'blog.quote',
  13. quote: 'Some interesting quote',
  14. author: 1,
  15. },
  16. ],
  17. opening_hours: [
  18. {
  19. day_interval: 'Mon',
  20. opening_hour: '7:00 PM',
  21. closing_hour: '11:00 PM',
  22. },
  23. {
  24. day_interval: 'Tue',
  25. opening_hour: '7:00 PM',
  26. closing_hour: '11:00 PM',
  27. },
  28. ],
  29. // pass the id of a media to link it to the entry
  30. cover: 1,
  31. // automatically creates the relations when passing the ids in the field
  32. reviews: [1, 2, 3],
  33. }
  34. );

When updating an entry with its components or dynamic zones beware that if you send the components without any id the previous components will be deleted and replaced. You can update the components by sending their id with the rest of the fields:

Update by id and update previous components

  1. strapi.query('restaurant').update(
  2. { id: 1 },
  3. {
  4. name: 'Mytitle',
  5. content: [
  6. {
  7. __component: 'blog.rich-text',
  8. id: 1,
  9. title: 'Some title',
  10. subTitle: 'Some sub title',
  11. },
  12. {
  13. __component: 'blog.quote',
  14. id: 1,
  15. quote: 'Some interesting quote',
  16. author: 1,
  17. },
  18. ],
  19. opening_hours: [
  20. {
  21. id: 2,
  22. day_interval: 'Mon',
  23. opening_hour: '7:00 PM',
  24. closing_hour: '11:00 PM',
  25. },
  26. {
  27. id: 1,
  28. day_interval: 'Tue',
  29. opening_hour: '7:00 PM',
  30. closing_hour: '11:00 PM',
  31. },
  32. ],
  33. // pass the id of a media to link it to the entry
  34. cover: 1,
  35. // automatically creates the relations when passing the ids in the field
  36. reviews: [1, 2, 3],
  37. }
  38. );

Partial update by name

  1. strapi.query('restaurant').update(
  2. { title: 'specific name' },
  3. {
  4. title: 'restaurant name',
  5. }
  6. );

delete

Deletes an entry and returns its value before deletion. You can delete multiple entries at once with the passed params.

Examples

Delete one by id

  1. strapi.query('restaurant').delete({ id: 1 });

Delete multiple by field

  1. strapi.query('restaurant').delete({ district: '_18th' });

count

Returns the count of entries matching Strapi filters.

Examples

Count by district

  1. strapi.query('restaurant').count({ district: '_1st' });

Count by name contains

  1. strapi.query('restaurant').count({ name_contains: 'food' });

Count by date less than

  1. strapi.query('restaurant').count({ date_lt: '2019-08-01T00:00:00Z' });

search

Returns entries based on a search on all fields allowing it. (this feature will return all entries on sqlite).

Examples

Search first ten starting at 20

  1. strapi.query('restaurant').search({ _q: 'my search query', _limit: 10, _start: 20 });

Search and sort

  1. strapi.query('restaurant').search({ _q: 'my search query', _limit: 100, _sort: 'date:desc' });

countSearch

Returns the total count of entries based on a search. (this feature will return all entries on sqlite).

Example
  1. strapi.query('restaurant').countSearch({ _q: 'my search query' });

Custom Queries

When you want to customize your services or create new ones you will have to build your queries with the underlying ORM models.

To access the underlying model:

  1. strapi.query(modelName, plugin).model;

Then you can run any queries available on the model. You should refer to the specific ORM documentation for more details:

Bookshelf

Documentation: https://bookshelfjs.org/Backend customization - 图4 (opens new window)

Example

  1. const result = await strapi
  2. .query('restaurant')
  3. .model.query(qb => {
  4. qb.where('id', 1);
  5. })
  6. .fetch();
  7. const fields = result.toJSON();

Knex

Knex.js can be used to build and make custom queries directly to the database.

Documentation: http://knexjs.org/#BuilderBackend customization - 图5 (opens new window)

You can access the Knex instance with:

  1. const knex = strapi.connections.default;

You can then use Knex to build your own custom queries. You will lose all the functionalities of the model, but this could come handy if you are building a more custom schema. Please note that if you are using the draft system, Strapi nullyfies all the Draft columns util they are published.

Example

  1. const _ = require('lodash');
  2. const knex = strapi.connections.default;
  3. const result = await knex('restaurants')
  4. .where('cities', 'berlin')
  5. .whereNot('cities.published_at', null)
  6. .join('chefs', 'restaurants.id', 'chefs.restaurant_id')
  7. .select('restaurants.name as restaurant')
  8. .select('chef.name as chef')
  9. // Lodash's groupBy method can be used to
  10. // return a grouped key-value object generated from
  11. // the response
  12. return (_.groupBy(result, 'chef');

We strongly suggest to sanitize any strings before making queries to the DB Never attempt to make a raw query with data coming straight from the front-end; if you were looking for raw queries, please refer to this sectionBackend customization - 图6 (opens new window) of the documentation.

Mongoose

Documentation: https://mongoosejs.com/Backend customization - 图7 (opens new window)

Example

  1. const result = strapi.query('restaurant').model.find({
  2. date: { $gte: '2019-01-01T00.00.00Z' },
  3. });
  4. const fields = result.map(entry => entry.toObject());

Models

Concepts

Content Type’s models

Models are a representation of the database’s structure. They are split into two separate files. A JavaScript file that contains the model options (e.g: lifecycle hooks), and a JSON file that represents the data structure stored in the database.

Path — ./api/restaurant/models/Restaurant.js.

  1. module.exports = {
  2. lifecycles: {
  3. // Called before an entry is created
  4. beforeCreate(data) {},
  5. // Called after an entry is created
  6. afterCreate(result) {},
  7. },
  8. };

Path — ./api/restaurant/models/Restaurant.settings.json.

  1. {
  2. "kind": "collectionType",
  3. "connection": "default",
  4. "info": {
  5. "name": "restaurant",
  6. "description": "This represents the Restaurant Model"
  7. },
  8. "attributes": {
  9. "cover": {
  10. "collection": "file",
  11. "via": "related",
  12. "plugin": "upload"
  13. },
  14. "name": {
  15. "default": "",
  16. "type": "string"
  17. },
  18. "description": {
  19. "default": "",
  20. "type": "text"
  21. }
  22. }
  23. }

In this example, there is a Restaurant model which contains the attributes cover, name and description.

Component’s models

Another type of model is named components. A component is a data structure that can be used in one or many other API’s model. There is no lifecycle related, only a JSON file definition.

Path — ./components/default/simple.json

  1. {
  2. "connection": "default",
  3. "collectionName": "components_default_simples",
  4. "info": {
  5. "name": "simple",
  6. "icon": "arrow-circle-right"
  7. },
  8. "options": {},
  9. "attributes": {
  10. "name": {
  11. "type": "string"
  12. }
  13. }
  14. }

In this example, there is a Simple component which contains the attribute name. And the component is in the category default.

Where are the models defined?

The Content Types models are defined in each ./api/**/models/ folder. Every JavaScript or JSON file in these folders will be loaded as a model. They are also available through the strapi.models and strapi.api.**.models global variables. Usable everywhere in the project, they contain the ORM model object that they refer to. By convention, a model’s name should be written in lowercase.

The Components models are defined in the ./components folder. Every component has to be inside a subfolder (the category name of the component).

How to create a model?

TIP

If you are just starting out it is very convenient to generate some models with the Content Type Builder directly in the admin interface. You can then review the generated model mappings on the code level. The UI takes over a lot of validation tasks and gives you a feeling for available features.

For Content Types models

Use the CLI and run the following command strapi generate:model restaurant name:string description:text.
Read the CLI documentation for more information.

This will create two files located at ./api/restaurant/models:

  • Restaurant.settings.json: contains the list of attributes and settings. The JSON format makes the file easily editable.
  • Restaurant.js: imports Restaurant.settings.json and extends it with additional settings and life cycle callbacks.

TIP

When you create a new API using the CLI (strapi generate:api <name>), a model is automatically created.

For Components models

To create a component you will have to use the Content Type Builder from the Admin panel, there is not a cli generator for components.

Or you can create your component manually by following the file path described previously and by following the file structure described below.

Model settings

Additional settings can be set on models:

  • kind (string) - Define if the model is a Collection Type (collectionType) of a Single Type (singleType) - only for Content Types
  • connection (string) - Connection name which must be used. Default value: default.
  • collectionName (string) - Collection name (or table name) in which the data should be stored.
  • globalId (string) - Global variable name for this model (case-sensitive) - only for Content Types
  • attributes (object) - Define the data structure of your model. Find available options below.

Path — Restaurant.settings.json.

  1. {
  2. "kind": "collectionType",
  3. "connection": "mongo",
  4. "collectionName": "Restaurants_v1",
  5. "globalId": "Restaurants",
  6. "attributes": {}
  7. }

In this example, the model Restaurant will be accessible through the Restaurants global variable. The data will be stored in the Restaurants_v1 collection or table and the model will use the mongo connection defined in ./config/database.js

WARNING

If not set manually in the JSON file, Strapi will adopt the filename as globalId. The globalId serves as a reference to your model within relations and Strapi APIs. If you chose to rename it (either by renaming your file or by changing the value of the globalId), you’d have to migrate your tables manually and update the references. Please note that you should not alter the Strapi’s models globalId (plugins and core models) since they are used directly within Strapi APIs and other models’ relations.

TIP

The connection value can be changed whenever you want, but you should be aware that there is no automatic data migration process. Also if the new connection doesn’t use the same ORM you will have to rewrite your queries.

Model information

The info key on the model-json states information about the model. This information is used in the admin interface, when showing the model.

  • name: The name of the model, as shown in admin interface.
  • description: The description of the model.
  • icon: The fontawesome V5 name - only for Components

Path — Restaurant.settings.json.

  1. {
  2. "info": {
  3. "name": "restaurant",
  4. "description": ""
  5. }
  6. }

Model options

The options key on the model-json states.

  • timestamps: This tells the model which attributes to use for timestamps. Accepts either boolean or Array of strings where first element is create date and second element is update date. Default value when set to true for Bookshelf is ["created_at", "updated_at"] and for MongoDB is ["createdAt", "updatedAt"].

  • privateAttributes: This configuration allows to treat a set of attributes as private, even if they’re not actually defined as attributes in the model. Accepts an Array of strings. It could be used to remove from API responses timestamps or _v when using MongoDB. The set of privateAttributes defined in the model are merged with the privateAttributes defined in the global Strapi configuration.

  • populateCreatorFields: Configure whether the API response should include created_by and updated_by fields or not. Accepts a boolean. The default value is false.

  • draftAndPublish: Enable the draft and publish feature. Accepts a boolean. The default value is false.

Path — Restaurant.settings.json.

  1. {
  2. "options": {
  3. "timestamps": true,
  4. "privateAttributes": ["id", "created_at"],
  5. "populateCreatorFields": true,
  6. "draftAndPublish": false
  7. }
  8. }

Define the attributes

The following types are currently available:

  • string
  • text
  • richtext
  • email
  • password
  • integer
  • biginteger
  • float
  • decimal
  • date
  • time
  • datetime
  • boolean
  • enumeration
  • json
  • uid

Validations

You can apply basic validations to attributes. The following supported validations are only supported by MongoDB database connections. If you’re using SQL databases, you should use the native SQL constraints to apply them.

  • required (boolean) — If true, adds a required validator for this property.
  • unique (boolean) — Whether to define a unique index on this property.
  • index (boolean) — Adds an index on this property, this will create a single field indexBackend customization - 图8 (opens new window) that will run in the background. Only supported by MongoDB.
  • 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.

Security validations

To improve the Developer Experience when developing or using the administration panel, the framework enhances the attributes with these “security validations”:

  • private (boolean) — If true, the attribute will be removed from the server response. (This is useful to hide sensitive data).
  • configurable (boolean) - If false, the attribute isn’t configurable from the Content Type Builder plugin.
  • autoPopulate (boolean) - If false, the related data will not populate within REST responses. (This will not stop querying the relational data on GraphQL)

Exceptions

uid

  • targetField(string) — The value is the name of an attribute that has string of the text type.
  • options (string) — The value is a set of options passed to the underlying uid generatorBackend customization - 图9 (opens new window). A caveat is that the resulting uid must abide to the following RegEx /^[A-Za-z0-9-_.~]*$.

Example

Path — Restaurant.settings.json.

  1. {
  2. ...
  3. "attributes": {
  4. "title": {
  5. "type": "string",
  6. "min": 3,
  7. "max": 99,
  8. "unique": true
  9. },
  10. "description": {
  11. "default": "My description",
  12. "type": "text",
  13. "required": true
  14. },
  15. "slug": {
  16. "type": "uid",
  17. "targetField": "title"
  18. }
  19. ...
  20. }
  21. }

Relations

Relations let you create links (relations) between your Content Types.

One-way relationships are useful to link one entry to one other entry. However, only one of the models can be queried with its linked item.

Example

A pet can be owned by someone (a user).

Path — ./api/pet/models/Pet.settings.json.

  1. {
  2. "attributes": {
  3. "owner": {
  4. "model": "user"
  5. }
  6. }
  7. }

Example

  1. // Create a pet
  2. const xhr = new XMLHttpRequest();
  3. xhr.open('POST', '/pets', true);
  4. xhr.setRequestHeader('Content-Type', 'application/json');
  5. xhr.send(
  6. JSON.stringify({
  7. owner: '5c151d9d5b1d55194d3209be', // The id of the user you want to link
  8. })
  9. );

Many-way relationships are useful to link one entry to many other entries. However, only one of the models can be queried with its linked items.

Example

A pet can be owned by many people (multiple users).

Path — ./api/pet/models/Pet.settings.json.

  1. {
  2. "attributes": {
  3. "owners": {
  4. "collection": "user"
  5. }
  6. }
  7. }

Example

  1. // Create a pet
  2. const xhr = new XMLHttpRequest();
  3. xhr.open('POST', '/pets', true);
  4. xhr.setRequestHeader('Content-Type', 'application/json');
  5. xhr.send(
  6. JSON.stringify({
  7. owners: ['5c151d9d5b1d55194d3209be', '5fc666a5bf16f48ed050ef5b'], // The id of the users you want to link
  8. })
  9. );

One-to-One relationships are useful when you have one entity that could be linked to only one other entity. And vice versa.

Example

A user can have one address. And this address is only related to this user.

Path — ./api/user/models/User.settings.json.

  1. {
  2. "attributes": {
  3. "address": {
  4. "model": "address",
  5. "via": "user"
  6. }
  7. }
  8. }

Path — ./api/address/models/Address.settings.json.

  1. {
  2. "attributes": {
  3. "user": {
  4. "model": "user"
  5. }
  6. }
  7. }

Example

  1. // Create an address
  2. const xhr = new XMLHttpRequest();
  3. xhr.open('POST', '/addresses', true);
  4. xhr.setRequestHeader('Content-Type', 'application/json');
  5. xhr.send(
  6. JSON.stringify({
  7. user: '5c151d9d5b1d55194d3209be', // The id of the user you want to link
  8. })
  9. );

One-to-Many relationships are useful when an entry can be linked to multiple entries of another Content Type. And an entry of the other Content Type can be linked to only one entry.

Example

A user can have many articles, and an article can be related to only one user (author).

Path — ./api/user/models/User.settings.json.

  1. {
  2. "attributes": {
  3. "articles": {
  4. "collection": "article",
  5. "via": "author"
  6. }
  7. }
  8. }

Path — ./api/article/models/Article.settings.json.

  1. {
  2. "attributes": {
  3. "author": {
  4. "model": "user"
  5. }
  6. }
  7. }

Examples

  1. // Create an article
  2. const xhr = new XMLHttpRequest();
  3. xhr.open('POST', '/articles', true);
  4. xhr.setRequestHeader('Content-Type', 'application/json');
  5. xhr.send(
  6. JSON.stringify({
  7. author: '5c151d9d5b1d55194d3209be', // The id of the user you want to link
  8. })
  9. );
  10. // Update an article
  11. const xhr = new XMLHttpRequest();
  12. xhr.open('PUT', '/users/5c151d9d5b1d55194d3209be', true);
  13. xhr.setRequestHeader('Content-Type', 'application/json');
  14. xhr.send(
  15. JSON.stringify({
  16. articles: ['5c151d51eb28fd19457189f6', '5c151d51eb28fd19457189f8'], // Set of ALL articles linked to the user (existing articles + new article or - removed article)
  17. })
  18. );

Many-to-Many relationships are useful when an entry can be linked to multiple entries of another Content Type. And an entry of the other Content Type can be linked to many entries.

Example

A product can be related to many categories and a category can have many products.

Path — ./api/product/models/Product.settings.json.

  1. {
  2. "attributes": {
  3. "categories": {
  4. "collection": "category",
  5. "via": "products",
  6. "dominant": true,
  7. "collectionName": "products_categories__categories_products" // optional
  8. }
  9. }
  10. }

NOTE: (NoSQL databases only) The dominant key defines which table/collection should store the array that defines the relationship. Because there are no join tables in NoSQL, this key is required for NoSQL databases (e.g. MongoDB).

NOTE: (NoSQL databases only) The collectionName key defines the name of the join table. It has to be specified once, in the dominant attribute of the relation. If it is not specified, Strapi will use a generated default one. It is useful to define the name of the join table when the name generated by Strapi is too long for the database you use.

Path — ./api/category/models/Category.settings.json.

  1. {
  2. "attributes": {
  3. "products": {
  4. "collection": "product",
  5. "via": "categories"
  6. }
  7. }
  8. }

Example

  1. // Update a product
  2. const xhr = new XMLHttpRequest();
  3. xhr.open('PUT', '/products/5c151d9d5b1d55194d3209be', true);
  4. xhr.setRequestHeader('Content-Type', 'application/json');
  5. xhr.send(
  6. JSON.stringify({
  7. categories: ['5c151d51eb28fd19457189f6', '5c151d51eb28fd19457189f8'], // Set of ALL categories linked to the product (existing categories + new category or - removed category).
  8. })
  9. );

Polymorphic relationships are the solution when you don’t know which kind of model will be associated to your entry, or when you want to connect different types of models to a model. A common use case is an Image model that can be associated to different types of models (Article, Product, User, etc.).

Single vs Many

Let’s stay with our Image model which might belong to a single Article or Product entry.

NOTE: In other words, it means that an Image entry can be associated to one entry. This entry can be a Article or Product entry.

Also our Image model might belong to many Article or Product entries.

NOTE: In other words, it means that an Article entry can relate to the same image as a Product entry.

Path — ./api/image/models/Image.settings.json.

  1. {
  2. "attributes": {
  3. "related": {
  4. "collection": "*",
  5. "filter": "field"
  6. }
  7. }
  8. }
Filter

The filter attribute is optional (but we highly recommend to use it every time). If it’s provided it adds a new match level to retrieve the related data.

For example, the Product model might have two attributes which are associated to the Image model. To distinguish which image is attached to the cover field and which images are attached to the pictures field, we need to save and provide this to the database.

Path — ./api/article/models/Product.settings.json.

  1. {
  2. "attributes": {
  3. "cover": {
  4. "model": "image",
  5. "via": "related"
  6. },
  7. "pictures": {
  8. "collection": "image",
  9. "via": "related"
  10. }
  11. }
  12. }

The value of the filter attribute is the name of the column where the information is stored.

Example

An Image model might belong to many Article models or Product models.

Path — ./api/image/models/Image.settings.json.

  1. {
  2. "attributes": {
  3. "related": {
  4. "collection": "*",
  5. "filter": "field"
  6. }
  7. }
  8. }

Path — ./api/article/models/Article.settings.json.

  1. {
  2. "attributes": {
  3. "avatar": {
  4. "model": "image",
  5. "via": "related"
  6. }
  7. }
  8. }

Path — ./api/article/models/Product.settings.json.

  1. {
  2. "attributes": {
  3. "pictures": {
  4. "collection": "image",
  5. "via": "related"
  6. }
  7. }
  8. }

Components

Component fields let your create a relation between your Content Type and a Component structure.

Example

Lets say we created an openinghours component in restaurant category.

Path — ./api/restaurant/models/Restaurant.settings.json.

  1. {
  2. "attributes": {
  3. "openinghours": {
  4. "type": "component",
  5. "repeatable": true,
  6. "component": "restaurant.openinghours"
  7. }
  8. }
  9. }
  • repeatable (boolean): Could be true or false that let you create a list of data.
  • component (string): It follows this format <category>.<componentName>.

Create a restaurant with non-repeatable component

  1. const xhr = new XMLHttpRequest();
  2. xhr.open('POST', '/restaurants', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. openinghour: {
  7. opening_at: '10am',
  8. closing_at: '6pm',
  9. day: 'monday',
  10. },
  11. })
  12. );

Create a restaurant with repeatable component

  1. const xhr = new XMLHttpRequest();
  2. xhr.open('POST', '/restaurants', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. openinghours: [
  7. {
  8. opening_at: '10am',
  9. closing_at: '6pm',
  10. day: 'monday',
  11. },
  12. {
  13. opening_at: '10am',
  14. closing_at: '6pm',
  15. day: 'tuesday',
  16. },
  17. ],
  18. })
  19. );

Update a restaurant with non-repeatable component

  1. const xhr = new XMLHttpRequest();
  2. xhr.open('PUT', '/restaurants/1', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. openinghour: {
  7. id: 1, // the ID of the entry
  8. opening_at: '11am',
  9. closing_at: '7pm',
  10. day: 'wednesday',
  11. },
  12. })
  13. );

Update a restaurant with repeatable component

  1. const xhr = new XMLHttpRequest();
  2. xhr.open('PUT', '/restaurants/2', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. openinghours: [
  7. {
  8. "id": 1 // the ID of the entry you want to update
  9. "opening_at": "10am",
  10. "closing_at": "6pm",
  11. "day": "monday"
  12. },
  13. {
  14. "id": 2, // you also have to put the ID of entries you don't want to update
  15. "opening_at": "10am",
  16. "closing_at": "6pm",
  17. "day": "tuesday"
  18. }
  19. ]
  20. })
  21. );

NOTE if you don’t specify the ID it will delete and re-create the entry and you will see the ID value change.

Delete a restaurant with non-repeatable component

  1. const xhr = new XMLHttpRequest();
  2. xhr.open('PUT', '/restaurants/1', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. openinghour: null,
  7. })
  8. );

Delete a restaurant with repeatable component

  1. const xhr = new XMLHttpRequest();
  2. xhr.open('PUT', '/restaurants/2', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. openinghours: [
  7. {
  8. "id": 1 // the ID of the entry you want to keep
  9. "opening_at": "10am",
  10. "closing_at": "6pm",
  11. "day": "monday"
  12. }
  13. ]
  14. })
  15. );

Dynamic Zone

Dynamic Zone fields let you create a flexible space in which to compose content, based on a mixed list of components.

Example

Lets say we created an slider and content component in article category.

Path — ./api/article/models/Article.settings.json.

  1. {
  2. "attributes": {
  3. "body": {
  4. "type": "dynamiczone",
  5. "components": ["article.slider", "article.content"]
  6. }
  7. }
  8. }
  • components (array): Array of components that follows this format <category>.<componentName>.
  1. const xhr = new XMLHttpRequest();
  2. xhr.open('POST', '/articles', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. body: [
  7. {
  8. __component: 'article.content',
  9. content: 'This is a content',
  10. },
  11. {
  12. __component: 'article.slider',
  13. name: 'Slider name',
  14. },
  15. ],
  16. })
  17. );
  1. const xhr = new XMLHttpRequest();
  2. xhr.open('PUT', '/restaurant/2', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. body: [
  7. {
  8. "id": 1 // the ID of the entry you want to update
  9. "__component": "article.content",
  10. "content": "This is an updated content",
  11. },
  12. {
  13. "id": 2, // you also have to put the ID of entries you don't want to update
  14. "__component": "article.slider",
  15. "name": "Slider name",
  16. }
  17. ]
  18. })
  19. );

NOTE if you don’t specify the ID it will delete and re-create the entry and you will see the ID value change.

  1. const xhr = new XMLHttpRequest();
  2. xhr.open('PUT', '/restaurant/2', true);
  3. xhr.setRequestHeader('Content-Type', 'application/json');
  4. xhr.send(
  5. JSON.stringify({
  6. body: [
  7. {
  8. "id": 1 // the ID of the entry you want to keep
  9. "__component": "article.content",
  10. "content": "This is an updated content",
  11. }
  12. ]
  13. })
  14. );

Lifecycle hooks

The lifecycle hooks are functions that get triggered when the Strapi queries are called. They will get triggered automatically when you manage your content in the Admin Panel or when you develop custom code using queries·

To configure a ContentType lifecycle hook you can set a lifecycles key in the {modelName}.js file located in the ./api/{apiName}/models folder.

Available Lifecycle hooks

beforeFind(params, populate)

Parameters:

NameTypeDescription
paramsObjectFind params (e.g: limit, filters)

afterFind(results, params, populate)

Parameters:

NameTypeDescription
resultsArray{Object}The results found for the find query
paramsObjectFind params (e.g: limit, filters)
populateArray{string}Populate specific relations

beforeFindOne(params, populate)

Parameters:

NameTypeDescription
paramsObjectFind params (e.g: filters)

afterFindOne(result, params, populate)

Parameters:

NameTypeDescription
resultObjectThe results found for the findOne query
paramsObjectFind params (e.g: filters)
populateArray{string}Populate specific relations

beforeCreate(data)

Parameters:

NameTypeDescription
dataObjectInput data to the entry was created with

afterCreate(result, data)

Parameters:

NameTypeDescription
resultObjectCreated entry
dataObjectInput data to the entry was created with

beforeUpdate(params, data)

Parameters:

NameTypeDescription
paramsObjectFind params (e.g: filters)
dataObjectInput data to the entry was created with

afterUpdate(result, params, data)

Parameters:

NameTypeDescription
resultObjectUpdated entry
paramsObjectFind params (e.g: filters)
dataObjectInput data to the entry was created with

beforeDelete(params)

Parameters:

NameTypeDescription
paramsObjectFind params (e.g: filters)

afterDelete(result, params)

Parameters:

NameTypeDescription
resultObjectDeleted entry
paramsObjectFind params (e.g: filters)

beforeCount(params)

Parameters:

NameTypeDescription
paramsObjectFind params (e.g: filters)

afterCount(result, params)

Parameters:

NameTypeDescription
resultIntegerThe count matching entries
paramsObjectFind params (e.g: filters)

beforeSearch(params, populate)

Parameters:

NameTypeDescription
paramsObjectFind params (e.g: filters)
populateArray{string}Populate specific relations

afterSearch(result, params)

Parameters:

NameTypeDescription
resultsArray{Object}The entries found
paramsObjectFind params (e.g: filters)
populateArray{string}Populate specific relations

beforeCountSearch(params)

Parameters:

NameTypeDescription
paramsObjectFind params (e.g: filters)

afterCountSearch(result, params)

Parameters:

NameTypeDescription
resultIntegerThe count matching entries
paramsObjectFind params (e.g: filters)

Example

Path — ./api/restaurant/models/Restaurant.js.

  1. module.exports = {
  2. /**
  3. * Triggered before user creation.
  4. */
  5. lifecycles: {
  6. async beforeCreate(data) {
  7. data.isTableFull = data.numOfPeople === 4;
  8. },
  9. },
  10. };

TIP

You can mutate one of the parameters to change its properties. Make sure not to reassign the parameter as it will have no effect:

This will Work

  1. module.exports = {
  2. lifecycles: {
  3. beforeCreate(data) {
  4. data.name = 'Some fixed name';
  5. },
  6. },
  7. };

This will NOT Work

  1. module.exports = {
  2. lifecycles: {
  3. beforeCreate(data) {
  4. data = {
  5. ...data,
  6. name: 'Some fixed name',
  7. };
  8. },
  9. },
  10. };

Custom use

When you are building custom ORM specific queries the lifecycles will not be triggered. You can however call a lifecycle function directly if you wish.

Bookshelf example

Path - ./api/{apiName}/services/{serviceName}.js

  1. module.exports = {
  2. async createCustomEntry() {
  3. const ORMModel = strapi.query(modelName).model;
  4. const newCustomEntry = await ORMModel.forge().save();
  5. // trigger manually
  6. ORMModel.lifecycles.afterCreate(newCustomEntry.toJSON());
  7. },
  8. };

TIP

When calling a lifecycle function directly, you will need to make sure you call it with the expected parameters.

Webhooks

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 inside the file ./extensions/users-permissions/models/User.js.

Available configurations

You can set webhook configurations inside the file ./config/server.js.

  • 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

  1. module.exports = {
  2. webhooks: {
  3. defaultHeaders: {
  4. 'Custom-Header': 'my-custom-header',
  5. },
  6. },
  7. };

Securing your webhooks

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 requests.

You can configure these global headers by updating the file at ./config/server.js:

  1. module.exports = {
  2. webhooks: {
  3. defaultHeaders: {
  4. Authorization: 'Bearer my-very-secured-token',
  5. },
  6. },
  7. };
  1. module.exports = {
  2. webhooks: {
  3. defaultHeaders: {
  4. Authorization: `Bearer ${process.env.WEBHOOK_TOKEN}`,
  5. },
  6. },
  7. };

If you are developing the webhook handler yourself you can now verify the token by reading the headers.

Available events

By default Strapi webhooks can be triggered by the following events:

NameDescription
entry.createTriggered when a Content Type entry is created.
entry.updateTriggered when a Content Type entry is updated.
entry.deleteTriggered when a Content Type entry is deleted.
entry.publishTriggered when a Content Type entry is published.
entry.unpublishTriggered when a Content Type entry is unpublished.
media.createTriggered when a media is created.
media.updateTriggered when a media is updated.
media.deleteTriggered when a media is deleted.

*only when draftAndPublish is enabled on this Content Type.

Payloads

NOTE

Private fields and passwords are not sent in the payload.

Headers

When a payload is delivered to your webhook’s URL, it will contain specific headers:

HeaderDescription
X-Strapi-EventName of the event type that was triggered.

entry.create

This event is triggered when a new entry is created.

Example payload

  1. {
  2. "event": "entry.create",
  3. "created_at": "2020-01-10T08:47:36.649Z",
  4. "model": "address",
  5. "entry": {
  6. "id": 1,
  7. "geolocation": {},
  8. "city": "Paris",
  9. "postal_code": null,
  10. "category": null,
  11. "full_name": "Paris",
  12. "created_at": "2020-01-10T08:47:36.264Z",
  13. "updated_at": "2020-01-10T08:47:36.264Z",
  14. "cover": null,
  15. "images": []
  16. }
  17. }

entry.update

This event is triggered when an entry is updated.

Example payload

  1. {
  2. "event": "entry.update",
  3. "created_at": "2020-01-10T08:58:26.563Z",
  4. "model": "address",
  5. "entry": {
  6. "id": 1,
  7. "geolocation": {},
  8. "city": "Paris",
  9. "postal_code": null,
  10. "category": null,
  11. "full_name": "Paris",
  12. "created_at": "2020-01-10T08:47:36.264Z",
  13. "updated_at": "2020-01-10T08:58:26.210Z",
  14. "cover": null,
  15. "images": []
  16. }
  17. }

entry.delete

This event is triggered when an entry is deleted.

Example payload

  1. {
  2. "event": "entry.delete",
  3. "created_at": "2020-01-10T08:59:35.796Z",
  4. "model": "address",
  5. "entry": {
  6. "id": 1,
  7. "geolocation": {},
  8. "city": "Paris",
  9. "postal_code": null,
  10. "category": null,
  11. "full_name": "Paris",
  12. "created_at": "2020-01-10T08:47:36.264Z",
  13. "updated_at": "2020-01-10T08:58:26.210Z",
  14. "cover": null,
  15. "images": []
  16. }
  17. }

entry.publish

This event is triggered when an entry is published.

Example payload

  1. {
  2. "event": "entry.publish",
  3. "created_at": "2020-01-10T08:59:35.796Z",
  4. "model": "address",
  5. "entry": {
  6. "id": 1,
  7. "geolocation": {},
  8. "city": "Paris",
  9. "postal_code": null,
  10. "category": null,
  11. "full_name": "Paris",
  12. "created_at": "2020-01-10T08:47:36.264Z",
  13. "updated_at": "2020-01-10T08:58:26.210Z",
  14. "published_at": "2020-08-29T14:20:12.134Z",
  15. "cover": null,
  16. "images": []
  17. }
  18. }

entry.unpublish

This event is triggered when an entry is unpublished.

Example payload

  1. {
  2. "event": "entry.unpublish",
  3. "created_at": "2020-01-10T08:59:35.796Z",
  4. "model": "address",
  5. "entry": {
  6. "id": 1,
  7. "geolocation": {},
  8. "city": "Paris",
  9. "postal_code": null,
  10. "category": null,
  11. "full_name": "Paris",
  12. "created_at": "2020-01-10T08:47:36.264Z",
  13. "updated_at": "2020-01-10T08:58:26.210Z",
  14. "published_at": null,
  15. "cover": null,
  16. "images": []
  17. }
  18. }

media.create

This event is triggered when you upload a file on entry creation or through the media interface.

Example payload

  1. {
  2. "event": "media.create",
  3. "created_at": "2020-01-10T10:58:41.115Z",
  4. "media": {
  5. "id": 1,
  6. "name": "image.png",
  7. "hash": "353fc98a19e44da9acf61d71b11895f9",
  8. "sha256": "huGUaFJhmcZRHLcxeQNKblh53vtSUXYaB16WSOe0Bdc",
  9. "ext": ".png",
  10. "mime": "image/png",
  11. "size": 228.19,
  12. "url": "/uploads/353fc98a19e44da9acf61d71b11895f9.png",
  13. "provider": "local",
  14. "provider_metadata": null,
  15. "created_at": "2020-01-10T10:58:41.095Z",
  16. "updated_at": "2020-01-10T10:58:41.095Z",
  17. "related": []
  18. }
  19. }

media.update

This event is triggered when you replace a media or update the metadata of a media through the media interface.

Example payload

  1. {
  2. "event": "media.update",
  3. "created_at": "2020-01-10T10:58:41.115Z",
  4. "media": {
  5. "id": 1,
  6. "name": "image.png",
  7. "hash": "353fc98a19e44da9acf61d71b11895f9",
  8. "sha256": "huGUaFJhmcZRHLcxeQNKblh53vtSUXYaB16WSOe0Bdc",
  9. "ext": ".png",
  10. "mime": "image/png",
  11. "size": 228.19,
  12. "url": "/uploads/353fc98a19e44da9acf61d71b11895f9.png",
  13. "provider": "local",
  14. "provider_metadata": null,
  15. "created_at": "2020-01-10T10:58:41.095Z",
  16. "updated_at": "2020-01-10T10:58:41.095Z",
  17. "related": []
  18. }
  19. }

media.delete

This event is triggered only when you delete a media through the media interface.

Example payload

  1. {
  2. "event": "media.delete",
  3. "created_at": "2020-01-10T11:02:46.232Z",
  4. "media": {
  5. "id": 11,
  6. "name": "photo.png",
  7. "hash": "43761478513a4c47a5fd4a03178cfccb",
  8. "sha256": "HrpDOKLFoSocilA6B0_icA9XXTSPR9heekt2SsHTZZE",
  9. "ext": ".png",
  10. "mime": "image/png",
  11. "size": 4947.76,
  12. "url": "/uploads/43761478513a4c47a5fd4a03178cfccb.png",
  13. "provider": "local",
  14. "provider_metadata": null,
  15. "created_at": "2020-01-07T19:34:32.168Z",
  16. "updated_at": "2020-01-07T19:34:32.168Z",
  17. "related": []
  18. }
  19. }