diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 6a251ed895..24b463928f 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -14,6 +14,7 @@ module.exports = { }, title: 'Strapi Docs', description: 'API creation made simple, secure and fast.', + base: '/documentation/', themeConfig: { versions: [ ['Version 3.x.x', '/3.x.x/'], diff --git a/docs/.vuepress/dist/1.x.x/SUMMARY.html b/docs/.vuepress/dist/1.x.x/SUMMARY.html new file mode 100644 index 0000000000..b390c82f57 --- /dev/null +++ b/docs/.vuepress/dist/1.x.x/SUMMARY.html @@ -0,0 +1,23 @@ + + +
+ + +Where is my data ?
This is a recurring question when you create web applications from scratch or +using a web framework. That's why Strapi provides a ready-to-use admin panel.
This part is a summary of your application.
Work in progress.
The Data Explorer allows you to easily manage your data.
+The UI is auto-generated depending on the models
of your application.
+So, in just a few seconds, you are able to create, search, view, edit and
+delete your data.
To try it, simply create a new API using the Studio or the CLI. +Then restart the server and reload the web browser page.
List, edit and delete the users information of your application.
Each user can be related to one or many roles.
The first registered user is automatically related to the admin
role.
Strapi contains a security system based on the routes of your application. +The admin panel allows you to visualize the different routes of your server and +to manage the security of each of them.
Public
: no level of security (anyone can use the route).Registered
: the user has to be logged to use the route.Owner
: the user must be one of the contributors
of the model
updated or deleted.Admin
: only the users related to the admin
role are allowed to access the route.The admin panel is developed with Angular.js, using the John PAPA styleguide.
+You can customize the admin from ./api/admin/public
in your generated application.
To build the admin panel:
bower
and gulp
with $ npm install gulp bower -g
.$ npm install
in this directory.$ gulp serve
.$ gulp dist
.
+That will update the files in the following folder: ./api/admin/public/dist
.If you change the default port (1337) of your server, you will have to update
+./api/admin/public/config/config.json
and then run $ npm install && gulp dist
+in ./api/admin/public
.
NOTE: You can create your own admin
generator using the .strapirc
file.
+Learn more how to use custom generators.
The blueprints are a set of useful actions containing all the logic you need to +create a clean RESTful API. The generated controllers and routes are automatically +plugged to the blueprint actions. Thanks to that, as soon as you generate a new API +from the CLI, you can enjoy a RESTful API without writing any line of code.
For example, if you generate a pet
API, you will be able to immediately visit
+POST /pet?name=joe
to create a pet, and visit GET /pet
to see an array
+of your application's pets.
Blueprints are great for prototyping, but they are also a powerful tool in production +due to their ability to be overridden, protected, extended or disabled entirely.
All of the following actions return a promise.
Returns a list of records from the model as a JSON array of objects.
Route:
{
+ "routes": {
+ "GET /pet": {
+ "controller": "Pet",
+ "action": "find"
+ }
+ }
+}
+
Controller function:
find: function * () {
+ this.model = 'Pet';
+
+ try {
+ this.body = yield strapi.hooks.blueprints.find(this);
+ } catch (error) {
+ this.body = error;
+ }
+}
+
Results may be filtered, paginated, and sorted based on the blueprint configuration +and/or parameters sent in the request.
Optional parameters:
*
(string): To filter results based on a particular attribute, specify a query
+parameter with the same name as the attribute defined on your model.where
(string): Instead of filtering based on a specific attribute, you may instead
+choose to provide a where
parameter with a Waterline WHERE
criteria object,
+encoded as a JSON string. This allows you to take advantage of contains
, startsWith
,
+and other sub-attribute criteria modifiers for more powerful find()
queries.limit
(number): The maximum number of records to send back (useful for pagination).
+Defaults to 30.skip
(number): The number of records to skip (useful for pagination).sort
(string): The sort order. By default, returned records are sorted by primary key value
+in ascending order. ASC
or DESC
.populate
(string): If specified, override the default automatic population process.
+Accepts a comma separated list of attributes names for which to populate record values.Returns a single record from the model as a JSON object.
Route:
{
+ "routes": {
+ "GET /pet/:id": {
+ "controller": "Pet",
+ "action": "findOne"
+ }
+ }
+}
+
Controller function:
findOne: function * () {
+ this.model = 'Pet';
+
+ try {
+ this.body = yield strapi.hooks.blueprints.findOne(this);
+ } catch (error) {
+ this.body = error;
+ }
+}
+
The findOne()
blueprint action returns a single record from the model as a JSON object.
+The specified id is the primary key of the desired record.
Required parameters:
id
(string or number): The desired record's primary key value.Creates a new model instance in your database then returns its values.
Route:
{
+ "routes": {
+ "POST /pet/:id": {
+ "controller": "Pet",
+ "action": "create"
+ }
+ }
+}
+
Controller function:
create: function * () {
+ this.model = 'Pet';
+
+ try {
+ this.body = yield strapi.hooks.blueprints.create(this);
+ } catch (error) {
+ this.body = error;
+ }
+}
+
Attributes can be sent in the HTTP body as form-encoded values or JSON.
The promise returned contains a JSON object representing the newly created instance.
+If a validation error occurred, a JSON response with the invalid attributes and
+the Context status is set to 400
.
Optional parameters:
*
(string, number, object or array): Pass in body parameter with the same
+name as the attribute defined in your model to set those values on your new record.
+Nested objects and arrays passed in as parameters are handled the same
+way as if they were passed into the model's .create()
method.Updates an existing record. Attributes to change should be sent in the HTTP body +as form-encoded values or JSON.
Route:
{
+ "routes": {
+ "PUT /pet/:id": {
+ "controller": "Pet",
+ "action": "update"
+ }
+ }
+}
+
Controller function:
update: function * () {
+ this.model = 'Pet';
+
+ try {
+ this.body = yield strapi.hooks.blueprints.update(this);
+ } catch (error) {
+ this.body = error;
+ }
+}
+
Updates the model instance which matches the id
parameter.
+The promise resolved contains a JSON object representing the newly updated instance.
+If a validation error occurred, a JSON response with the invalid attributes and a
+400
status code will be returned instead. If no model instance exists matching the
+specified id
, a 404
is returned.
Required parameters:
id
(string or number): The desired record's primary key value.Optional parameters:
*
(string, number, object or array): Pass in body parameter with the same
+name as the attribute defined on your model to set those values on your new record.
+Nested objects and arrays passed in as parameters are handled the same
+way as if they were passed into the model's .update()
method.Deletes an existing record specified by id
from the database forever and returns
+the values of the deleted record.
Route:
{
+ "routes": {
+ "DELETE /pet/:id": {
+ "controller": "Pet",
+ "action": "destroy"
+ }
+ }
+}
+
Controller function:
destroy: function * () {
+ this.model = 'Pet';
+
+ try {
+ this.body = yield strapi.hooks.blueprints.destroy(this);
+ } catch (error) {
+ this.body = error;
+ }
+}
+
Destroys the model instance which matches the id
parameter.
+Responds with a JSON object representing the newly destroyed instance.
+If no model instance exists matching the specified id
, the Context status is set to 400 and the returned promise is rejected.
Required parameters:
id
(string or number): The desired record's primary key value.Adds an association between two records.
Route:
{
+ "routes": {
+ "POST /pet/:id/:parentId/:relation": {
+ "controller": "Pet",
+ "action": "add"
+ }
+ }
+}
+
Controller function:
add: function * () {
+ this.model = 'Pet';
+
+ try {
+ this.body = yield strapi.hooks.blueprints.add(this);
+ } catch (error) {
+ this.body = error;
+ }
+}
+
This action pushes a reference to some other record (the "foreign" record) onto a +collection attribute of this record (the "primary" record).
:relation
of an existing record is supplied, it will be associated with
+the primary record.:relation
is supplied, and the body of the POST
contains values for a
+new record, that record will be created and associated with the primary record.via
on both sides) the association
+on the foreign record will also be updated.Notes:
update
blueprint.Removes an association between two records.
Route:
{
+ "routes": {
+ "DELETE /pet/:id/:parentId/:relation/:id": {
+ "controller": "Pet",
+ "action": "remove"
+ }
+ }
+}
+
Controller function:
remove: function * () {
+ this.model = 'Pet';
+
+ try {
+ this.body = yield strapi.hooks.blueprints.remove(this);
+ } catch (error) {
+ this.body = error;
+ }
+}
+
This action removes a reference to some other record (the "foreign" record) +from a collection attribute of this record (the "primary" record).
via
on both sides)
+the association on the foreign record will also be updated.+ ← + Views + + Context + → +
Strapi comes with a convenient command-line tool to quickly get your application scaffolded and running.
$ strapi login
+
Ask your Strapi Studio credentials to link your new applications on your machine to +the Strapi Studio aiming to have a perfect workflow while you build APIs.
Go to the Strapi Studio to start the experience.
$ strapi new <appName>
+
Create a new Strapi project in a directory called appName
.
$ strapi new
is really just a special generator which runs strapi-generate-new
.
+In other words, running $ strapi new <appName>
is an alias for running
+$ strapi generate new <appName>
, and like any Strapi generator, the actual generator module
+which gets run can be overridden.
$ cd <appName>
+$ strapi start
+
Run the Strapi application in the current directory.
+If ./node_modules/strapi
exists, it will be used instead of the globally installed module Strapi.
$ cd <appName>
+$ strapi console
+
Start your Strapi application, and enter the Node.js REPL. This means you can access +and use all of your models, services, configuration, and much more. Useful for trying out +Waterline queries, quickly managing your data, and checking out your project's runtime configuration.
Note that this command still starts the server, so your routes will be accessible via HTTP and sockets.
Strapi exposes the same global variables in the console as it does in your application code.
+This is particularly useful in the REPL. By default, you have access to the Strapi application
+instance, your models as well as Lodash (_
) and Socket.IO (io
).
$ strapi generate api <apiName>
+
Generate a complete API with controllers, models and routes.
$ strapi version
+
Output the current globally installed Strapi version.
$ strapi link
+
Link an existing application without an appId
to the Strapi Studio.
This command can be useful if you were not logged into the Studio or if you +didn't have Internet access when you generated your application.
$ strapi logout
+
If you don't want to be logged in to the Strapi Studio anymore.
+ ← + Upload + + Customization + → +
While Strapi dutifully adheres to the philosophy of convention-over-configuration, +it is important to understand how to customize those handy defaults from time to time. +For almost every convention in Strapi, there is an accompanying set of configuration +options that allow you to adjust or override things to fit your needs.
Settings specified at the root directory will be available in all environments.
If you'd like to have some settings take effect only in certain environments,
+you can use the special environment-specific files and folders.
+Any files saved under the ./config/environments/development
directory will be
+loaded only when Strapi is started in the development
environment.
The built-in meaning of the settings in strapi.config
are, in some cases,
+only interpreted by Strapi during the start
process. In other words, changing some
+options at runtime will have no effect. To change the port your application is running on,
+for instance, you can't just change strapi.config.port
. You'll need to change or
+override the setting in a configuration file or as a command-line argument,
+then restart the server.
strapi.config
merge user config from the ./config
directory with the package.json
+of the application.
The most important things in your package.json
are the name and version fields.
+Those are actually required, and your package won't install without them.
+The name and version together form an identifier that is assumed to be completely unique.
The name of the application.
name
./package.json
string
Notes:
package.json
file.require()
, so it should be something short,
+but also reasonably descriptive. You may want to check the npm registry to see if there's something
+by that name already, before you get too attached to it. https://www.npmjs.com/@myorg/mypackage
.Changes to the package should come along with changes to the version.
version
./package.json
string
Notes:
node-semver
, which is bundled with npm as a dependency.The description of your application helps people discover your package, as it's listed in npm search
.
description
./package.json
string
Public assets refer to static files on your server that you want to make accessible to the
+outside world. In Strapi, these files are placed in the ./public
directory.
Strapi is compatible with any front-end strategy; whether it's Angular, Backbone, Ember, +iOS, Android, Windows Phone, or something else that hasn't been invented yet.
Key: static
Environment: all
Location: ./config/general.json
Type: boolean
Defaults to:
{
+ "static": true
+}
+
Notes:
false
to disable the public assets.Key: views
Environment: all
Location: ./config/general.json
Type: object
Defaults to:
{
+ "views": false
+}
+
For more information, please refer to the views documentation.
Options:
map
: Object mapping extension names to engine names.default
: Default extension name to use when missing.cache
: When true
compiled template functions will be cached in-memory,
+this prevents subsequent disk I/O, as well as the additional compilation step
+that most template engines peform. By default this is enabled when the NODE_ENV
+environment variable is anything but development
, such as stage
or production
.Notes:
false
to disable views support.Socket.IO enables real-time bidirectional event-based communication. +It works on every platform, browser or device, focusing equally on reliability +and speed.
By default Strapi binds Socket.IO and your common websockets features are
+available using the io
object.
Key: websockets
Environment: all
Location: ./config/general.json
Type: boolean
Defaults to:
{
+ "websockets": true
+}
+
Notes:
false
to disable websockets with Socket.IO.Set a favicon for your web application.
Key: favicon
Environment: all
Location: ./config/general.json
Type: object
Defaults to:
{
+ "favicon": {
+ "path": "favicon.ico",
+ "maxAge": 86400000
+ }
+}
+
Options:
path
(string): Relative path for the favicon to use from the application root directory.maxAge
(integer): Cache-control max-age directive. Set to pass the cache-control in ms.Notes:
false
to disable the favicon feature.Prefix your API aiming to not have any conflicts with your front-end if you have one of if need to +for some other reasons.
Key: prefix
Environment: all
Location: ./config/general.json
Type: string
Defaults to:
{
+ "prefix": ""
+}
+
Notes:
/
, e.g. /api
.The blueprints are a set of useful actions containing all the logic you need to +create a clean RESTful API. The generated controllers and routes are automatically +plugged to the blueprint actions. Thanks to that, as soon as you generate a new API +from the CLI, you can enjoy a RESTful API without writing any line of code.
Key: blueprints
Environment: all
Location: ./config/general.json
Type: object
Defaults to:
{
+ "blueprints": {
+ "defaultLimit": 30,
+ "populate": true
+ }
+}
+
Options:
defaultLimit
(integer): The maximum number of records to send back.populate
(boolean): If enabled, the population process fills out attributes
+in the returned list of records according to the model's defined associations.If your application will touch people or systems from all over the world, internationalization
+and localization (i18n
) may be an important part of your international strategy.
Strapi provides built-in support for detecting user language preferences and translating +static words/sentences.
Key: i18n
Environment: all
Location: ./config/i18n.json
Type: object
Defaults to:
{
+ "i18n": {
+ "defaultLocale": "en",
+ "modes": [
+ "query",
+ "subdomain",
+ "cookie",
+ "header",
+ "url",
+ "tld"
+ ],
+ "cookieName": "locale"
+ }
+}
+
Options:
defaultLocale
(string): The default locale to use.modes
(array): Accept locale variable from:
+query
: detect query string with /?locale=fr
subdomain
: detect subdomain with fr.myapp.com
cookie
: detect cookie with Accept-Language: en,fr;q=0.5
header
: detect header with Cookie: locale=fr
url
: detect url with /fr
tld
: detect TLD with myapp.fr
cookieName
(string): i18n cookies property, tries to find a cookie named locale
here.
+Allows the locale to be set from query string or from cookie.Notes:
false
to disable the locales feature../config/locales
directory.For convenience, Strapi exposes a handful of global variables. By default, your application's
+models, the global strapi
object and the Lodash node module are all available on the global
+scope; meaning you can refer to them by name anywhere in your backend code
+(as long as Strapi has been loaded).
Nothing in Strapi core relies on these global variables. Each and every global exposed in
+Strapi may be disabled in strapi.config.globals
.
Bear in mind that none of the globals, including strapi
, are accessible until after
+Strapi has loaded. In other words, you won't be able to use strapi.models.car
or Car
+outside of a function (since Strapi will not have finished loading yet).
Key: globals
Environment: all
Location: ./config/globals.json
Type: object
Defaults to:
{
+ "globals": {
+ "models": true,
+ "strapi": true,
+ "async": true,
+ "_": true,
+ "graphql": true
+ }
+}
+
Options:
models
(boolean): Your application's models are exposed as global variables using their globalId
.
+For instance, the model defined in the file ./api/car/models/Car.js
will be globally accessible as Car
.strapi
(boolean): In most cases, you will want to keep the strapi
object globally accessible,
+it makes your application code much cleaner.async
(boolean): Exposes an instance of Async._
(boolean): Exposes an instance of Lodash.graphql
(boolean): Exposes an instance of GraphQL.Notes:
false
to disable global variables.The bootstrap function is a server-side JavaScript file that is executed by Strapi +just before your application is started.
This gives you an opportunity to set up your data model, run jobs, or perform some special logic.
bootstrap
./config/functions/bootstrap.js
function
Notes:
CRON tasks allow you to schedule jobs (arbitrary functions) for execution at specific dates, +with optional recurrence rules. It only uses a single timer at any given time +(rather than reevaluating upcoming jobs every second/minute).
Key: cron
Environment: all
Location: ./config/functions/cron.js
Type: object
module.exports.cron = {
+
+ /**
+ * Every day at midnight.
+ */
+
+ '0 0 * * *': function () {
+ // Your code here
+ }
+ };
+}
+
Notes:
The Strapi Studio is a toolbox for developers that allows you to build and manage +your APIs in realtime without writing any line of code. When your application is +linked to the Studio, you are able to generate APIs from the Studio and see +the changes in realtime in your local application.
Key: studio
Environment: all
Location: ./config/studio.json
Type: object
Defaults to:
{
+ "studio": {
+ "enabled": true,
+ "secretKey": "YOUR SECRET KEY HERE"
+ }
+}
+
Options:
enabled
(boolean): Do you want your application linked to the Strapi Studio?secretKey
(string): The secret key of your application to link your
+current application with the Strapi Studio.The host name the connection was configured to.
Key: host
Environment: development
Location: ./config/environments/development/server.json
Type: string
Defaults to:
{
+ "host": "localhost"
+}
+
Notes:
host
in a production
environment.localhost
.The actual port assigned after the server has been started.
Key: port
Environment: development
Location: ./config/environments/development/server.json
Type: integer
Defaults to:
{
+ "port": 1337
+}
+
Notes:
host
in a production
environment.process.env.PORT
+value. If no port specified, the port will be 1337
.This is the URL of your front-end application.
This config key is useful when you don't use the ./public
directory for your
+assets or when you run your automation tools such as Gulp or Grunt on an other port.
This address can be resourceful when you need to redirect the user after he +logged in with an authentication provider.
Key: frontendUrl
Environment: development
Location: ./config/environments/development/server.json
Type: string
Defaults to:
{
+ "frontendUrl": ""
+}
+
Enable or disable auto-reload when your application crashes.
Key: reload
Environment: development
Location: ./config/environments/development/server.json
Type: object
Defaults to:
{
+ "reload": {
+ "timeout": 1000,
+ "workers": 1
+ }
+}
+
Options:
timeout
(integer): Set the timeout before killing a worker in ms.workers
(integer): Set the number of workers to spawn.
+If the workers
key is not defined, Strapi will use every free CPU
+(recommended in production
environement).Notes:
false
to disable the auto-reload and clustering features.Enable or disable request logs.
Key: logger
Environment: development
Location: ./config/environments/development/server.json
Type: boolean
Defaults to:
{
+ "logger": true
+}
+
Notes:
false
to disable the logger.Parse request bodies.
Key: parser
Environment: development
Location: ./config/environments/development/server.json
Type: object
Defaults to:
{
+ "parser": {
+ "encode": "utf-8",
+ "formLimit": "56kb",
+ "jsonLimit": "1mb",
+ "strict": true,
+ "extendTypes": {
+ "json": [
+ "application/x-javascript"
+ ]
+ }
+ }
+}
+
Options:
encode
(string): Requested encoding.formLimit
(string): Limit of the urlencoded body.
+If the body ends up being larger than this limit, a 413 error code is returned.jsonLimit
(string): Limit of the JSON body.strict
(boolean): When set to true
, JSON parser will only accept arrays and objects.extendTypes
(array): Support extend types.Notes:
false
to disable the body parser.Enable or disable Gzip compression.
Key: gzip
Environment: development
Location: ./config/environments/development/server.json
Type: boolean
Defaults to:
{
+ "gzip": true
+}
+
Notes:
false
to disable Gzip.The X-Response-Time
header records the response time for requests in HTTP servers.
+The response time is defined here as the elapsed time from when a request enters the application
+to when the headers are written out to the client.
Key: responseTime
Environment: development
Location: ./config/environments/development/reponse.json
Type: boolean
Defaults to:
{
+ "responseTime": true
+}
+
Notes:
false
to disable the response time header.Strapi comes installed with a powerful ORM/ODM called Waterline, a datastore-agnostic tool that +dramatically simplifies interaction with one or more databases.
It provides an abstraction layer on top of the underlying database, allowing you to easily query +and manipulate your data without writing vendor-specific integration code.
Key: orm
Environment: development
Location: ./config/environments/development/databases.json
Type: object
Defaults to:
{
+ "orm": {
+ "adapters": {
+ "disk": "sails-disk"
+ },
+ "defaultConnection": "default",
+ "connections": {
+ "default": {
+ "adapter": "disk",
+ "filePath": ".tmp/",
+ "fileName": "default.db",
+ "migrate": "alter"
+ },
+ "permanent": {
+ "adapter": "disk",
+ "filePath": "./data/",
+ "fileName": "permanent.db",
+ "migrate": "alter"
+ }
+ }
+ }
+}
+
Options:
adapters
(object): Association between a connection and the adapter to use.defaultConnection
(string): The default connection will be used if the
+connection
key of a model is empty or missing.connections
(object): Options of the connection.
+Every adapter has its own options such as host
, port
, database
, etc.
+The migrate
option controls how Strapi will attempt to automatically
+rebuild the tables/collections/sets/etc. in your schema.
+safe
: never auto-migrate database(s).alter
: auto-migrate database(s), but attempt to keep existing data.drop
: drop all data and rebuild models every time your application starts.Notes:
migrate
flag tells waterline what to do with data when the data is corrupt.
+You can set this flag to safe
which will ignore the corrupt data and continue to start.drop
, or even alter
, you risk losing your data. Be careful.
+Never use drop
or alter
with a production dataset.
+Additionally, on large databases alter
may take a long time to complete at startup.
+This may cause the start process to appear to hang.Since HTTP driven applications are stateless, sessions provide a way to store information +about the user across requests.
Strapi provides "guest" sessions, meaning any visitor will have a session,
+authenticated or not. If a session is new a Set-Cookie
will be produced regardless
+of populating the session.
Strapi only supports cookie sessions, for now.
Key: session
Environment: development
Location: ./config/environments/development/security.json
Type: object
Defaults to:
{
+ "session": {
+ "key": "myApp",
+ "secretKeys": [
+ "mySecretKey1"
+ ],
+ "maxAge": 86400000
+ }
+}
+
Options:
key
(string): The cookie name.secretKeys
(array): Keys used to encrypt the session cookie.maxAge
(integer): Sets the time in seconds for when a cookie will be deleted.Notes:
false
to disable sessions.CSRF is a type of attack which forces an end user to execute unwanted actions on a web +application backend with which he/she is currently authenticated.
Strapi bundles optional CSRF protection out of the box.
Key: csrf
Environment: development
Location: ./config/environments/development/security.json
Type: object
Defaults to:
{
+ "csrf": false
+}
+
Options:
key
(string): The name of the CSRF token added to the model.
+Defaults to _csrf
.secret
(string): The key to place on the session object which maps to the server side token.
+Defaults to _csrfSecret
.Notes:
false
to disable CSRF headers.POST
, PUT
, or DELETE
+requests, you'll need to acquire a CSRF token and include it as a parameter or header in those requests.Content Security Policy (CSP) is a W3C specification for instructing the client browser as to +which location and/or which type of resources are allowed to be loaded.
This spec uses "directives" to define a loading behaviors for target resource types. +Directives can be specified using HTTP response headers or or HTML Meta tags.
Key: csp
Environment: development
Location: ./config/environments/development/security.json
Type: object
Defaults to:
{
+ "csp": false
+}
+
Options:
policy
(object): Object definition of policy.reportOnly
(boolean): Enable report only mode.reportUri
(string): URI where to send the report data.Notes:
false
to disable CSP headers.Enables X-Frame-Options
headers to help prevent Clickjacking.
Key: xframe
Environment: development
Location: ./config/environments/development/security.json
Type: string
Defaults to:
{
+ "xframe": "SAMEORIGIN"
+}
+
Notes:
DENY
, SAMEORIGIN
or ALLOW-FROM
.false
to disable X-Frame-Options headers.Platform for Privacy Preferences (P3P) is a browser/web standard designed to facilitate +better consumer web privacy control. Currently out of all the major browsers, it is only +supported by Internet Explorer. It comes into play most often when dealing with legacy applications.
Key: p3p
Environment: development
Location: ./config/environments/development/security.json
Type: string
Defaults to:
{
+ "p3p": false
+}
+
Notes:
false
to disable P3P.Enables HTTP Strict Transport Security for the host domain.
The preload flag is required for HSTS domain submissions to Chrome's HSTS preload list.
Key: hsts
Environment: development
Location: ./config/environments/development/security.json
Type: object
Defaults to:
{
+ "hsts": {
+ "maxAge": 31536000,
+ "includeSubDomains": true
+ }
+}
+
Options:
maxAge
(integer): Number of seconds HSTS is in effect.includeSubDomains
(boolean): Applies HSTS to all subdomains of the host.Notes:
false
to disable HSTS.Cross-site scripting (XSS) is a type of attack in which a malicious agent manages to inject +client-side JavaScript into your website, so that it runs in the trusted environment of your users' browsers.
Enables X-XSS-Protection
headers to help prevent cross site scripting (XSS) attacks in older IE browsers (IE8).
Key: xssProtection
Environment: development
Location: ./config/environments/development/security.json
Type: object
Defaults to:
{
+ "xssProtection": false
+}
+
Options:
enabled
(boolean): If the header is enabled or not.mode
(string): Mode to set on the header.Notes:
false
to disable HTTP Strict Transport Security.Cross-origin resource sharing (CORS) is a mechanism that allows restricted resources +(e.g. fonts, JavaScript, etc.) on a web page to be requested from another domain outside +the domain from which the resource originated.
Key: cors
Environment: development
Location: ./config/environments/development/security.json
Type: object
Defaults to:
{
+ "cors": {
+ "origin": true,
+ "expose": [
+ "WWW-Authenticate",
+ "Server-Authorization"
+ ],
+ "maxAge": 31536000,
+ "credentials": true,
+ "methods": [
+ "GET",
+ "POST",
+ "PUT",
+ "DELETE",
+ "OPTIONS",
+ "HEAD"
+ ],
+ "headers": [
+ "Content-Type",
+ "Authorization"
+ ]
+ }
+}
+
Options:
origin
(string|boolean): Configures the Access-Control-Allow-Origin
CORS header.
+Expects a string (ex: http://example.com
) or a boolean.
+Set to true
to reflect the request origin, as defined by req.header('Origin')
.
+Set to false
to disable CORS.expose
(array): Configures the Access-Control-Expose-Headers
CORS header.
+Set this to pass the header, otherwise it is omitted.maxAge
(integer): Configures the Access-Control-Max-Age
CORS header.
+Set to an integer to pass the header, otherwise it is omitted.credentials
(boolean): Configures the Access-Control-Allow-Credentials
CORS header.
+Set to true
to pass the header, otherwise it is omitted.methods
(array): Configures the Access-Control-Allow-Methods
CORS header.headers
(array): Configures the Access-Control-Allow-Headers
CORS header.
+If not specified, defaults to reflecting the headers specified in the request's
+Access-Control-Request-Headers
header.Notes:
false
to disable CORS.Secure Sockets Layer (SSL), is a cryptographic protocol designed to provide communications security +over a computer network.
This configuration enforce SSL for your application.
Key: ssl
Environment: development
Location: ./config/environments/development/security.json
Type: object
Defaults to:
{
+ "ssl": false
+}
+
Options:
disabled
(boolean): If true
, this middleware will allow all requests through.trustProxy
(boolean): If true
, trust the X-Forwarded-Proto
header.Notes:
false
to disable SSL.The IP filter configuration allows you to whitelist or blacklist specific or range IP addresses.
The blacklisted IP addresses won't have access to your web application at all.
Key: ip
Environment: development
Location: ./config/environments/development/security.json
Type: object
Defaults to:
{
+ "ip": {
+ "whiteList": [],
+ "blackList": []
+ }
+}
+
Options:
whiteList
(array): IP addresses allowed.blackList
(array): IP addresses forbidden.Notes:
false
to disable IP filter.A proxy server is a server that acts as an intermediary for requests from clients +seeking resources from other servers.
Request your server, fetch the proxy URL you typed and return.
Key: proxy
Environment: development
Location: ./config/environments/development/security.json
Type: string
Defaults to:
{
+ "proxy": false
+}
+
Notes:
false
to disable the proxy security.A Strapi Context
encapsulates Node's request
and response
objects
+into a single object which provides many helpful methods for writing
+web applications and APIs.
These operations are used so frequently in HTTP server development +that they are added at this level instead of a higher level framework, +which would force middleware to re-implement this common functionality.
A Context
is created per request, and is referenced in middleware
+as the receiver, or the this
identifier, as shown in the following
+snippet:
strapi.app.use(function * () {
+ this; // is the Context
+ this.request; // is a Strapi Request
+ this.response; // is a Strapi Response
+});
+
Many of the context's accessors and methods simply delegate to their ctx.request
or ctx.response
+equivalents for convenience, and are otherwise identical. For example ctx.type
and ctx.length
+delegate to the response
object, and ctx.path
and ctx.method
delegate to the request
.
Context
specific methods and accessors.
Node's request
object.
Node's response
object.
Bypassing Strapi's response handling is not supported. Avoid using the following Node properties:
res.statusCode
res.writeHead()
res.write()
res.end()
A Strapi Request
object.
A Strapi Response
object.
The recommended namespace for passing information through middleware and to your front-end views.
this.state.user = yield User.find(id);
+
Application instance reference.
Get cookie name
with options
:
signed
the cookie requested should be signedStrapi uses the cookies module where options are simply passed.
Set cookie name
to value
with options
:
signed
sign the cookie valueexpires
a Date
for cookie expirationpath
cookie path, /'
by defaultdomain
cookie domainsecure
secure cookiehttpOnly
server-accessible cookie, true
by defaultStrapi uses the cookies module where options are simply passed.
Helper method to throw an error with a .status
property
+defaulting to 500
that will allow Strapi to respond appropriately.
+The following combinations are allowed:
this.throw(403);
+this.throw('name required', 400);
+this.throw(400, 'name required');
+this.throw('something exploded');
+
For example this.throw('name required', 400)
is equivalent to:
const err = new Error('name required');
+err.status = 400;
+throw err;
+
Note that these are user-level errors and are flagged with
+err.expose
meaning the messages are appropriate for
+client responses, which is typically not the case for
+error messages since you do not want to leak failure
+details.
You may optionally pass a properties
object which is merged into the error as-is,
+useful for decorating machine-friendly errors which are reported to the requester upstream.
this.throw(401, 'access_denied', { user: user });
+this.throw('access_denied', { user: user });
+
Strapi uses http-errors to create errors.
Helper method to throw an error similar to .throw()
+when !value
. Similar to Node's assert()
+method.
this.assert(this.state.user, 401, 'User not found. Please login!');
+
Strapi uses http-assert for assertions.
To bypass Strapi's built-in response handling, you may explicitly set this.respond = false;
.
+Use this if you want to write to the raw res
object instead of letting Strapi handle
+the response for you.
Note that using this is not supported by Strapi. This may break intended functionality
+of Strapi middleware and Strapi itself. Using this property is considered a hack and is
+only a convenience to those wishing to use traditional fn(req, res)
functions and middleware
+within Strapi.
The following accessors and alias Request equivalents:
ctx.header
ctx.headers
ctx.method
ctx.method=
ctx.url
ctx.url=
ctx.originalUrl
ctx.origin
ctx.href
ctx.path
ctx.path=
ctx.query
ctx.query=
ctx.querystring
ctx.querystring=
ctx.host
ctx.hostname
ctx.fresh
ctx.stale
ctx.socket
ctx.protocol
ctx.secure
ctx.ip
ctx.ips
ctx.subdomains
ctx.is()
ctx.accepts()
ctx.acceptsEncodings()
ctx.acceptsCharsets()
ctx.acceptsLanguages()
ctx.get()
The following accessors and alias Response equivalents:
ctx.body
ctx.body=
ctx.status
ctx.status=
ctx.message
ctx.message=
ctx.length=
ctx.length
ctx.type=
ctx.type
ctx.headerSent
ctx.redirect()
ctx.attachment()
ctx.set()
ctx.append()
ctx.remove()
ctx.lastModified=
ctx.etag=
+ ← + Blueprints + + GraphQL + → +
In keeping with the Node.js philosophy, Strapi aims to keep its core as small +as possible, delegating all but the most critical functions to separate modules.
Generators are designed to make it easier to customize the $ strapi new
+and $ strapi generate
command-line tools, and provide better support
+for different user features, custom admin panel, configuration options,
+view engines, etc.
Custom generators are linked to your machine aiming to have your personal +configuration and features at any time, for every application.
You can edit your custom generators inside the .strapirc
file at $HOME
.
First, make sure you this file exists:
$ strapi config
+
This file should look like this:
{
+ "generators": {
+
+ }
+}
+
At this time, you don't have any custom generators on your machine.
In your .strapirc
file, a custom generator is an object with three keys:
repository
: the Git repository to clone.remote
: the current remote to pull updates from.branch
: the branch you want to pull updates from.For example, to add a custom blog
generator, follow this:
{
+ "generators": {
+ "blog": {
+ "repository": "git@github.com:username/strapi-generate-blog.git",
+ "remote": "origin",
+ "branch": "master"
+ }
+ }
+}
+
Once you have updated your .strapirc
file, you need to clone and/or update your
+generators. To do so, just execute:
$ strapi update
+
This command will clone every new repository written in your configuration file +and pull the latest updates for the other ones.
Then, you can generate your blog
files inside your project with:
$ strapi generate blog
+
+ ← + CLI + + Internationalization + → +
Strapi contains a set of tools to send emails. This part is based on the +famous email node module: Nodemailer.
To change the STMP config, edit the ./api/email/config/environments/development/smtp.json
file.
{
+ "smtp": {
+ "from": "test<no-reply@test.com>",
+ "service": {
+ "name": "",
+ "user": "",
+ "pass": ""
+ }
+ }
+}
+
Options:
from
(string): The email address used to send emails.service
(object): The SMTP service info:
+name
(string): Name of the service used to send emails (eg. Gmail
).user
(string): Username of the service used (eg. john@gmail.com
).pass
(string): Password of the username used (eg. 12356
).The email service allows you to easily send emails from anywhere in your application.
Usage as a promise (yieldable) :
strapi.api.email.services.email.send({
+ from: 'contact@company.com', // Sender (defaults to `strapi.config.smtp.from`).
+ to: ['john@doe.com'], // Recipients list.
+ html: '<p>Hello John</p>', // HTML version of the email content.
+ text: 'Hello John' // Text version of the email content.
+ })
+ .then(function (data) {
+ console.log(data);
+ })
+ .catch(function (err) {
+ console.log(err);
+ });
+
Usage with a callback :
strapi.api.email.services.email.send({
+ from: 'contact@company.com', // Sender (defaults to `strapi.config.smtp.from`).
+ to: ['john@doe.com'], // Recipients list.
+ html: '<p>Hello John</p>', // HTML version of the email content.
+ text: 'Hello John' // Text version of the email content.
+ }, function (err, data) {
+ if (err) {
+ console.log(err);
+ } else {
+ console.log(data);
+ }
+ });
+
The email API is a simple API which can be used from your client (front-end, mobile...) application.
Route used to send emails:
POST /email
+
Request payload:
{
+ from: 'contact@company.com', // Optional : sender (defaults to `strapi.config.smtp.from`).
+ to: ['john@doe.com'], // Recipients list.
+ html: '<p>Hello John</p>', // HTML version of the email content.
+ text: 'Hello John' // Text version of the email content.
+}
+
Response payload:
{
+ "sent": true,
+ "from": "contact@company.com",
+ "to": "john@doe.com",
+ "html": "<p>Hello John</p>",
+ "text": "Hello John",
+ "template": "default",
+ "lang": "en",
+ "createdAt": "2015-10-21T09:10:36.486Z",
+ "updatedAt": "2015-10-21T09:10:36.871Z",
+ "id": 2
+}
+
Each sent email is registered in the database. So you can retrieve them whenever +you want. However, you can disable this option by overriding the email service logic.
+ ← + Configuration + + Introduction + → +
GraphQL is a data querying language that allows you to execute complex nested +requests between your clients and server applications.
By default, GraphQL is enabled and the HTTP endpoint is /graphql
.
+You can override this settings in the ./config/general.json
file.
{
+ "graphql": {
+ "enabled": true,
+ "route": "/graphql"
+ }
+}
+
Options:
enabled
(boolean): Enabled or disabled GraphQL.route
(string): Change GraphQL endpoint.Note: If GraphQL is disabled, the GraphQL global variable is not exposed.
Strapi takes over GraphQL natively. We added a function called query
to execute
+your query without given as a parameters the GraphQL schemas each time.
An example of how to use query
function:
// Build your query
+const query = '{ users{firstName lastName posts{title}} }';
+
+// Execute the query
+graphql.query(query)
+ .then(function (result) {
+ console.log(result);
+ })
+ .catch(function (error) {
+ console.log(error);
+ });
+
And the JSON result:
{
+ "users": [{
+ "firstname": "John",
+ "lastname": "Doe",
+ "posts":[{
+ "title": "First title..."
+ }, {
+ "title": "Second title..."
+ }, {
+ "title": "Third title..."
+ }]
+ }, {
+ "firstname": "Karl",
+ "lastname": "Doe",
+ "posts":[{
+ "title": "Fourth title..."
+ }]
+ }]
+}
+
Strapi also provides a HTTP GraphQL server to execute request from your front-end application.
An example of how to execute the same request as above with a HTTP request with jQuery.
$.get('http://yourserver.com/graphql?query={ users{firstName lastName posts{title}} }', function (data) {
+ console.log(data);
+});
+
If you're using Waterline ORM installed by default with Strapi, you have access to
+some Waterline query parameters in your GraphQL query such as sort
, limit
or skip
.
+Strapi also provides the start
and end
parameters to select records between two dates.
This example will return 10 users' records sorted alphabetically by firstName
:
const query = '{ users(limit: 10, sort: "firstName ASC"){firstName lastName post{title}} }';
+
You can access to the 10 next users by adding the skip
parameter:
const query = '{ users(limit: 10, sort: "firstName ASC", skip: 10){firstName lastName posts{title}} }';
+
And you also can select those records in a period between two dates with the start
and end
parameters:
const query = '{ users(limit: 10, sort: "firstName ASC", skip: 10, start: "09/21/2015", end:" 09/22/2015"){firstName lastName posts{title}} }';
+
Strapi comes with a powerful set of useful functions such as getLatest<Model>
, getFirst<Model>
and count<Model>
.
Returns the 5 latest users from the September 27th 2015 at 8:59:59 PM:
const query = '{ getLatestUsers(count: 5, start: "9/27/2015 20:59:59"){firstName lastName posts{title}} }';
+
Returns the 5 first users:
const query = '{ getFirstUsers(count: 5){firstName lastName posts{title}} }';
+
Returns the number of subscribers the September 28th 2015:
const query = '{ countUsers(start: "9/28/2015", end: "9/28/2015") }';
+
+ ← + Context + + Logging + → +
Strapi enables developers to focus on writing reusable application logic instead of spending time +building infrastructure. It is designed for building practical, production-ready Node.js applications +in a matter of hours instead of weeks.
The framework sits on top of Koa. Its ensemble of small modules work +together to provide simplicity, maintainability, and structural conventions to Node.js applications.
DISCLAIMER: This version is maintained for criticals issues only.
Install the latest stable release with the npm command-line tool:
$ npm install strapi -g
+
We advise you to use our Studio to build APIs. To do so, you need to create a Strapi account. +Go to the Strapi Studio to signup. +Studio is dedicated to developers to build applications without writing +any single line of code thanks to its powerful set of tools.
After creating an account on the Strapi Studio, you are able to link your machine to your +Strapi Studio account to get access to all features offered by the Strapi ecosystem. +Use your Strapi account credentials.
$ strapi login
+
You now are able to use the Strapi CLI. Simply create your first application and start the server:
$ strapi new <appName>
+
Note that you can generate a dry application using the dry
option:
$ strapi new <appName> --dry
+
This will generate a Strapi application without:
user
, email
and upload
APIs,grant
hook,waterline
and blueprints
hooks disabled),studio
hook disabled).This feature allows you to only use Strapi for your HTTP server structure if you want to.
$ cd <appName>
+$ strapi start
+
The default home page is accessible at http://localhost:1337/.
The Strapi ecosystem offers you two possibilities to create a complete RESTful API.
$ strapi generate api <apiName>
+
For example, you can create a car
API with a name (name
), year (year
) and
+the license plate (license
) with:
$ strapi generate api car name:string year:integer license:string
+
The Strapi Studio allows you to easily build and manage your application environment +thanks to a powerful User Interface.
Log into the Strapi Studio with your user account (http://studio.strapi.io) +and follow the instructions to start the experience.
Simply manage your APIs and relations thanks to the Strapi Studio.
Strapi comes with a simple but yet powerful dashboard.
Create, read, update and delete your data.
Manage user settings, login, registration, groups and permissions on the fly.
Strapi provides built-in support for detecting user language preferences and translating +static words/sentences.
Settings for localization/internationalization may be configured in strapi.config.i18n
.
+The most common reason you'll need to modify these settings is to edit the list of your
+application's supported locales and/or the location of your translation stringfiles.
Strapi reads JSON-formatted translation files from your project's ./config/locales
+directory. Each file corresponds with a locale (usually a language) that your backend will support.
+These files contain locale-specific strings (as JSON key-value pairs) that you can use in your
+views, controllers, etc.
When your server is in production
mode it will read these files only once and then cache
+the result. It will not write any updated strings when in production
mode.
Otherwise, the files will be read on every instantiation of the i18n
object.
+Additionally newly-detected strings will be automatically added, and written out,
+to the locale JSON files.
These files contain locale-specific strings (as JSON key-value pairs) that you can use in your views,
+controllers, etc. Here is an example locale file (./config/locales/fr.json
):
{
+ "Hello!": "Bonjour!",
+ "Hello %s, how are you today?": "Bonjour %s, comment allez-vous aujourd'hui ?"
+}
+
Note that the keys in your stringfiles are case sensitive and require exact matches. +There are a few different schools of thought on the best approach here, and it really depends on +who/how often you'll be editing the stringfiles in the future. Especially if you'll be +editing the translations by hand, simpler, all-lowercase key names may be preferable for maintainability.
For example, here's another pass at ./config/locales/fr.json
:
{
+ "hello": "Bonjour!",
+ "hello-how-are-you-today": "Bonjour %s, comment allez-vous aujourd'hui ?"
+}
+
And here's ./config/locales/en.json
:
{
+ "hello": "Hello!",
+ "hello-how-are-you-today": "Hello %s, how are you today?"
+}
+
You can also nest locale strings. But a better approach would be to use .
to represent nested strings.
+For example, here's the list of labels for the index page of a user controller:
{
+ "user.index.label.id": "User ID",
+ "user.index.label.name": "User Name"
+}
+
Locales are accessible from everywhere in your application.
this.body = this.i18n.__('hello-how-are-you-today', 'John');
+// => "Hello John, how are you today?"
+
Different plural forms are supported as a response to count
with this.i18n.__n(one, other, count)
.
Use this.i18n.__n()
as you would use this.i18.__()
directly:
this.body = this.i18n.__n('%s cat', '%s cats', 1);
+// => "1 cat"
+
+this.body = this.i18n.__n('%s cat', '%s cats', 3);
+// => "3 cats"
+
Or from locales:
{
+ "catEat": {
+ "one": "%d cat eats the %s",
+ "other": '%d cats eat the %s'
+ }
+}
+
this.body = this.i18n.__n('catEat', 10, 'mouse');
+// => "10 cats eat the mouse"
+
+ ← + Customization + + Models + → +
Important Note: Strapi 1.x is on maintenance only. Development focuses on the upcoming Strapi 3.0.
Strapi is an open-source Node.js rich framework for building applications and services.
Strapi enables developers to focus on writing reusable application logic instead of spending time +building infrastructure. It is designed for building practical, production-ready Node.js applications +in a matter of hours instead of weeks.
The framework sits on top of Koa. Its ensemble of small modules work +together to provide simplicity, maintainability, and structural conventions to Node.js applications.
Install the latest stable release with the npm command-line tool:
$ npm install strapi -g
+
You now are able to use the Strapi CLI. Simply create your first application and start the server:
$ strapi new <appName>
+$ cd <appName>
+$ strapi start
+
The default home page is accessible at http://localhost:1337/.
$ strapi generate api <apiName>
+
For example, you can create a car
API with a name (name
), year (year
) and
+the license plate (license
) with:
$ strapi generate api car name:string year:integer license:string
+
Note that you can generate a dry application using the dry
option:
$ strapi new <appName> --dry
+
This will generate a Strapi application without:
user
, email
and upload
APIs,grant
hook,waterline
and blueprints
hooks disabled),studio
hook disabled).This feature allows you to only use Strapi for your HTTP server structure if you want to.
The Strapi Studio allows you to easily build and manage your application environment +thanks to a powerful User Interface.
Log into the Strapi Studio with your user account (http://studio.strapi.io) +and follow the instructions to start the experience.
We advise you to use our Studio to build APIs. To do so, you need to create a Strapi account. +Go to the Strapi Studio to signup. +Studio is dedicated to developers to build applications without writing +any single line of code thanks to its powerful set of tools.
After creating an account on the Strapi Studio, you are able to link your machine to your +Strapi Studio account to get access to all features offered by the Strapi ecosystem. +Use your Strapi account credentials.
$ strapi login
+
Building on top of Strapi means your application is written entirely in JavaScript, +the language you and your team are already using in the browser.
Since you spend less time context-shifting, you're able to write code in a more consistent style, +which makes development more productive.
The entire Strapi framework is written in ES2015.
Strapi provides a robust layer for fundamental web applications to help you write your business +logic, without obscuring Node.js features that you know and love. Our goal is to make writing +business logic much easier than other frameworks.
Strapi comes with a generator that help jumpstart your application's backend without writing any code. Just run:
$ strapi generate api car
+
and you'll get an API that lets you read, paginate, sort, filter, create, destroy, update, +and associate cars.
We take security very seriously. This is why Strapi comes with several security layers that just work +depending on your needs. Strapi provides configuration for CORS, CSRF, CSP, X-Frame-Options, XSS, HSTS, +HTTPS, SSL, proxy, IP filtering and ships reusable security policies.
No matter what you need to secure, Strapi is the right tool to make it right.
Strapi comes installed with a powerful ORM/ODM called Waterline, a datastore-agnostic tool that +dramatically simplifies interaction with one or more databases.
It provides an abstraction layer on top of the underlying database, allowing you to easily query +and manipulate your data without writing vendor-specific integration code.
Strapi offers a new take on the familiar relational model, aimed at making data modeling more practical. +You can do all the same things you might be used to (one-to-many, many-to-many), but you can also assign +multiple named associations per-model. Better yet, you can assign different models to different databases, +and your associations/joins will still work, even across NoSQL and relational boundries.
Strapi has no problem implicitly/automatically joining a SQL table with a NoSQL collection and vice versa.
Strapi is compatible with any front-end strategy; whether it's Angular, Backbone, Ember, +iOS, Android, Windows Phone, or something else that hasn't been invented yet.
Plus it's easy to serve up the same API to be consumed by another web service or community of developers.
Convention over configuration is a consistent approach makes developing applications more +predictable and efficient for everybody involved.
If anyone on your team has worked with frameworks, Strapi will feel pretty familiar. +Not only that, but they can look at a Strapi project and know, generally, how to code up the basic +patterns they've implemented over and over again in the past; whether their background. +What about your second application, or your third? Each time you create a new Strapi application, +you start with a sane, familiar boilerplate that makes you more productive.
Configuration files give you extra opportunities for human error.
In many cases, you'll even be able to recycle some of your code.
By default outputs all errors to stderr
unless NODE_ENV
is test
.
+To perform custom error-handling logic such as centralized logging you can add an "error" event listener:
strapi.app.on('error', function (err) {
+ strapi.log.error('server error', err);
+});
+
If an error in the req/res cycle and it is not possible to respond to the client,
+the Context
instance is also passed:
strapi.app.on('error', function (err, ctx) {
+ strapi.log.error('server error', err, ctx);
+});
+
When an error occurs and it is still possible to respond to the client, +aka no data has been written to the socket, Strapi will respond appropriately with +a 500 "Internal Server Error". In either case an app-level "error" is emitted for logging purposes.
Strapi has built in support for the idea of having a different set of settings for each environment. +Real applications have this too, but often the framework around them doesn't accommodate it and +you end up having to swap configuration files in and out to achieve the same effect.
Strapi is flexible enough to allow you to explore and create when you have the time to but also +provides automation tools when you don't.
+ ← + Email + + Query Interface + → +
Strapi comes with a simple and useful built-in logger.
+Its usage is purposely very similar to console.log()
, but with a handful of
+extra features; namely support for multiple log levels with colorized,
+prefixed console output.
The logger is accessible through the strapi
object directly with strapi.log
.
You can work with this logger in the same way that you work with the default logger:
strapi.log.info('Logs work!');
+
In addition to logging string messages, the logger will also optionally log additional +JSON metadata objects. Adding metadata is simple:
strapi.log.info('Test log message', {
+ anything: 'This is metadata'
+});
+
The log method provides the same string interpolation methods like util.format
.
This allows for the following log messages.
strapi.log.info('test message %s', 'my string');
+// => info: test message my string
+
strapi.log.info('test message %d', 123);
+// => info: test message 123
+
strapi.log.info('test message %j', {
+ number: 123
+}, {});
+// => info: test message {"number":123}
+// => meta = {}
+
strapi.log.info('test message %s, %s', 'first', 'second', {
+ number: 123
+});
+// => info: test message first, second
+// => meta = {number: 123}
+
strapi.log.info('test message', 'first', 'second', {
+ number: 123
+});
+// => info: test message first second
+// => meta = {number: 123}
+
strapi.log.info('test message %s, %s', 'first', 'second', {
+ number: 123
+}, function() {});
+// => info: test message first, second
+// => meta = {number: 123}
+// => callback = function() {}
+
strapi.log.info('test message', 'first', 'second', {
+ number: 123
+}, function() {});
+// => info: test message first second
+// => meta = {number: 123}
+// => callback = function() {}
+
Setting the level for your logging message can be accomplished by using +the level specified methods defined.
strapi.log.debug('This is a debug log');
+strapi.log.info('This is an info log');
+strapi.log.warn('This is a warning log');
+strapi.log.error('This is an error log ');
+
+ ← + GraphQL + + Router + → +
Strapi comes installed with a powerful Object-Relational-Mapper (ORM) called Waterline, +a datastore-agnostic tool that dramatically simplifies interaction with one or more databases.
Models represent a structure of data which requires persistent storage. The data may live in any data-store +but is interfaced in the same way. This allows your users to live in PostgreSQL and your user preferences +to live in MongoDB and you will interact with the data models in the exact same way.
If you're using MySQL, a model might correspond to a table. If you're using MongoDB, it might correspond +to a collection. In either case, the goal is to provide a simple, modular way of managing data without +relying on any one type of database.
Models are defined in the ./api/<apiName>/models
directory.
The following properties can be specified at the top level of your model definition to override +the defaults for that particular model.
For example, this a basic model Pet
:
{
+ "identity": "pet",
+ "connection": "mongoDBServer",
+ "schema": true,
+ "attributes": {
+ "name": {
+ "type": "string",
+ "required": true
+ },
+ "gender": {
+ "type": "string",
+ "enum": ["male", "female"]
+ },
+ "age": {
+ "type": "int",
+ "max": 100
+ },
+ "birthDate": {
+ "type": "date"
+ },
+ "breed": {
+ "type": "string"
+ }
+ },
+ "autoPK": true,
+ "autoCreatedAt": true,
+ "autoUpdatedAt": true
+}
+
+
A flag to toggle schemaless or schema mode in databases that support schemaless data structures. +If turned off, this will allow you to store arbitrary data in a record. If turned on, only attributes +defined in the model's attributes object will be stored.
For adapters that don't require a schema, such as MongoDB or Redis, the schema
key is set to false
.
{
+ "schema": true|false
+}
+
The configured database connection where this model will fetch and save its data.
+Defaults to defaultSQLite
, the default connection that uses the waterline-sqlite3
adapter.
{
+ "connection": "mongoDBServer"
+}
+
The lowercase unique key for the model. By default, a model's identity is inferred automatically +by lowercasing its filename. You should never change this property on your models.
{
+ "identity": "petModel"
+}
+
This flag changes the global name by which you can access your model (if the globalization of models +is enabled). You should never change this property on your models.
{
+ "globaId": "pets"
+}
+
For example to access to your model function:
Pets.find().exec(function (error, pets) {
+ if (error) {
+ console.log(error);
+ return false;
+ }
+
+ console.log(pets);
+});
+
A flag to toggle the automatic definition of a primary key in your model.
+The details of this default primary key vary between adapters. In any case, the primary keys generated
+by autoPK
will be unique. If turned off no primary key will be created by default, and you will need
+to define one manually using primaryKey: true
for one of the model attributes.
{
+ "autoPK": true|false
+}
+
A flag to toggle the automatic definition of a createdAt
attribute in your model.
+By default, createdAt
is an attribute which will be automatically set when a record is created with
+the current timestamp.
{
+ "autoCreatedAt": true|false
+}
+
A flag to toggle the automatic definition of a updatedAt
attribute in your model.
+By default, updatedAt
is an attribute which will be automatically set with the current timestamp
+every time a record is updated.
{
+ "autoUpdatedAt": true|false
+}
+
You can define a custom name for the physical collection in your adapter by adding a tableName
+attribute. This isn't just for tables. In MySQL, PostgreSQL, Oracle, etc. this setting refers
+to the name of the table, but in MongoDB or Redis, it refers to the collection, and so forth.
+If no tableName
is specified, Waterline will use the model's identity
as its tableName
.
This is particularly useful for working with pre-existing/legacy databases.
{
+ "tableName": "pets_table"
+}
+
Model attributes are basic pieces of information about a model.
+A model called Pet
might have attributes called name
, gender
, age
,
+birthday
and breed
.
Options can be used to enforce various constraints and add special enhancements to model attributes.
Specifies the type of data that will be stored in this attribute. One of:
string
text
integer
float
date
datetime
boolean
binary
array
json
Defaults to string
if not specified.
Strapi bundles support for automatic validations of your models' attributes. +Any time a record is updated, or a new record is created, the data for each attribute will +be checked against all of your predefined validation rules. This provides a convenient failsafe +to ensure that invalid entries don't make their way into your application's database(s).
Validations are defined directly in your collection attributes.
after
(date): Checks if string date in this record is after the specified Date
.
+Must be valid JavaScript Date
.alpha
(boolean): Checks if string in this record contains only letters (a-zA-Z).alphadashed
(boolean): Checks if string in this record contains only numbers and/or dashes.alphanumeric
(boolean): Checks if string in this record contains only letters and numbers.alphanumericdashed
(boolean): Checks if string in this record contains only numbers and/or
+letters and/or dashes.array
(boolean): Checks if this record is a valid JavaScript array object.
+Strings formatted as arrays will fail.before
(date): Checks if string in this record is a date that's before the specified date.binary
(boolean): Checks if this record is a valid binary data. Strings will pass.boolean
(boolean): Checks if this record is a valid boolean. Strings will fail.contains
(string): Checks if string in this record contains the seed.creditcard
(boolean): Checks if string in this record is a credit card.date
(boolean): Checks if string in this record is a date takes both strings and JavaScript.datetime
(boolean): Checks if string in this record looks like a JavaScript datetime
.decimal
(boolean): Checks if it contains a decimal or is less than 1.email
(boolean): Checks if string in this record looks like an email address.empty
(boolean): Checks if the entry is empty. Arrays, strings, or arguments objects with
+a length of 0 and objects with no
+own enumerable properties are considered empty.equals
(integer): Checks if string in this record is equal to the specified value.
+They must match in both value and type.falsey
(boolean): Would a Javascript engine register a value of false
on this?.finite
(boolean): Checks if given value is, or can be coerced to, a finite number.
+This is not the same as native isFinite
+which will return true
for booleans and empty strings.float
(boolean): Checks if string in this record is of the number type float.hexadecimal
(boolean): Checks if string in this record is a hexadecimal number.hexColor
(boolean): Checks if string in this record is a hexadecimal color.in
(array): Checks if string in this record is in the specified array of allowed
+string values.int
(boolean): Check if string in this record is an integer.integer
(boolean): Check if string in this record is an integer. Alias for int
.ip
(boolean): Checks if string in this record is a valid IP (v4 or v6).ipv4
(boolean): Checks if string in this record is a valid IP v4.ipv6
(boolean): Checks if string in this record is aa valid IP v6.json
(boolean): Checks if the record is a JSON.lowercase
(boolean): Check if string in this record is in all lowercase.max
(integer): max value for an integer.maxLength
(integer):min
(integer): min value for an integer.minLength
(integer):notContains
(string): Checks if string in this record doesn't contain the seed.notIn
(array): does the value of this model attribute exist inside of the defined
+validator value (of the same type).
+Takes strings and arrays.notNull
(boolean): does this not have a value of null
?.null
(boolean): Checks if string in this record is null.number
(boolean): Checks if this record is a number. NaN
is considered a number.numeric
(boolean): Checks if string in this record contains only numbers.object
(boolean): Checks if this attribute is the language type of Object.
+Passes for arrays, functions, objects,
+regexes, new Number(0), and new String('') !regex
(regex): Checks if the record matches the specific regex.required
(boolean): Must this model attribute contain valid data before a new
+record can be created?.string
(boolean): Checks if the record is a string.text
(boolean): Checks if the record is a text.truthy
(boolean): Would a Javascript engine register a value of false
on this?undefined
(boolean): Would a JavaScript engine register this thing as have the
+value undefined
?uppercase
(boolean): Checks if string in this record is uppercase.url
(boolean): Checks if string in this record is a URL.urlish
(boolean): Checks if string in this record contains something that looks like
+a route, ending with a file extension.uuid
(boolean): Checks if string in this record is a UUID (v3, v4, or v5).uuidv3
(boolean): Checks if string in this record is a UUID (v3).uuidv4
(boolean): Checks if string in this record is a UUID (v4).When a record is created, if no value was supplied, the record will be created with the specified
+defaultsTo
value.
"attributes": {
+ "usersGroup": {
+ "type": "string",
+ "defaultsTo": "guess"
+ }
+}
+
Sets up the attribute as an auto-increment key. When a new record is added to the model, +if a value for this attribute is not specified, it will be generated by incrementing the most recent +record's value by one.
Attributes which specify autoIncrement
should always be of type: integer
.
+Also, bear in mind that the level of support varies across different datastores.
+For instance, MySQL will not allow more than one auto-incrementing column per table.
"attributes": {
+ "placeInLine": {
+ "type": "integer",
+ "autoIncrement": true
+ }
+}
+
Ensures no two records will be allowed with the same value for the target attribute. +This is an adapter-level constraint, so in most cases this will result in a unique index on the +attribute being created in the underlying datastore.
Defaults to false
if not specified.
"attributes": {
+ "username": {
+ "type": "string",
+ "unique": true
+ }
+}
+
Use this attribute as the the primary key for the record. Only one attribute per model can be the
+primaryKey
. Defaults to false
if not specified.
This should never be used unless autoPK
is set to false
.
"attributes": {
+ "uuid": {
+ "type": "string",
+ "primaryKey": true,
+ "required": true
+ }
+}
+
A special validation property which only saves data which matches a whitelisted set of values.
"attributes": {
+ "gender": {
+ "type": "string",
+ "enum": ["male", "female"]
+ }
+}
+
If supported in the adapter, can be used to define the size of the attribute.
+For example in MySQL, size
can be specified as a number (n
) to create a column with the SQL
+data type: varchar(n)
.
"attributes": {
+ "name": {
+ "type": "string",
+ "size": 24
+ }
+}
+
Inside an attribute definition, you can specify a columnName
to force Waterline to store data
+for that attribute in a specific column in the configured connection.
+Be aware that this is not necessarily SQL-specific. It will also work for MongoDB fields, etc.
While the columnName
property is primarily designed for working with existing/legacy databases,
+it can also be useful in situations where your database is being shared by other applications,
+or you don't have access permissions to change the schema.
"attributes": {
+ "name": {
+ "type": "string",
+ "columnName": "pet_name"
+ }
+}
+
With Waterline you can associate models with other models across all data stores. +This means that your users can live in PostgreSQL and their photos can live in MongoDB +and you can interact with the data as if they lived together on the same database. +You can also have associations that live on separate connections or in different databases +within the same adapter.
A one-way association is where a model is associated with another model. +You could query that model and populate to get the associated model. +You can't however query the associated model and populate to get the associating model.
In this example, we are associating a User
with a Pet
but not a Pet
with a User
.
+Because we have only formed an association on one of the models, a Pet
has no restrictions
+on the number of User
models it can belong to. If we wanted to, we could change this and
+associate the Pet
with exactly one User
and the User
with exactly one Pet
.
./api/pet/models/Pet.settings.json
:
{
+ "attributes": {
+ "name": {
+ "type": "string",
+ "required": true,
+ "unique": true
+ },
+ "color": {
+ "type": "string",
+ "required": true
+ }
+ }
+}
+
./api/user/models/User.settings.json
:
{
+ "attributes": {
+ "name": {
+ "type": "string",
+ "required": true,
+ "unique": true
+ },
+ "color": {
+ "type": "string",
+ "required": true
+ },
+ "pony": {
+ "model": "pet"
+ }
+ }
+}
+
A one-to-one association states that a model may only be associated with one other model. +In order for the model to know which other model it is associated with a foreign key must +be included in the record.
In this example, we are associating a Pet
with a User
. The User
may only have one
+Pet
and viceversa, a Pet
can only have one User
. However, in order to query this association
+from both sides, you will have to create/update both models.
./api/pet/models/Pet.settings.json
:
{
+ "attributes": {
+ "name": {
+ "type": "string",
+ "required": true,
+ "unique": true
+ },
+ "color": {
+ "type": "string",
+ "required": true
+ },
+ "owner": {
+ "model": "user"
+ }
+ }
+}
+
./api/user/models/User.settings.json
:
{
+ "attributes": {
+ "name": {
+ "type": "string",
+ "required": true,
+ "unique": true
+ },
+ "age": {
+ "type": "integer",
+ "required": true
+ },
+ "pony": {
+ "model": "pet"
+ }
+ }
+}
+
A one-to-many association states that a model can be associated with many other models.
+To build this association a virtual attribute is added to a model using the collection
property.
+In a one-to-many association one side must have a collection
attribute and the other side must contain a
+model
attribute. This allows the many side to know which records it needs to get when a populate
is used.
Because you may want a model to have multiple one-to-many associations on another model a via
key is
+needed on the collection
attribute. This states which model
attribute on the one side of the association
+is used to populate the records.
In this example, a User
can have several Pet
, but a Pet
has only one owner
(from the User
model).
./api/pet/models/Pet.settings.json
:
{
+ "attributes": {
+ "name": {
+ "type": "string",
+ "required": true,
+ "unique": true
+ },
+ "color": {
+ "type": "string",
+ "required": true
+ },
+ "owner": {
+ "model": "user"
+ }
+ }
+}
+
./api/user/models/User.settings.json
:
{
+ "attributes": {
+ "name": {
+ "type": "string",
+ "required": true,
+ "unique": true
+ },
+ "age": {
+ "type": "integer",
+ "required": true
+ },
+ "pets": {
+ "collection": "pet",
+ "via": "owner"
+ }
+ }
+}
+
A many-to-many association states that a model can be associated with many other models +and vice-versa. Because both models can have many related models a new join table will +need to be created to keep track of these relations.
Waterline will look at your models and if it finds that two models both have collection
+attributes that point to each other, it will automatically build up a join table for you.
Because you may want a model to have multiple many-to-many associations on another model
+a via
key is needed on the collection
attribute. This states which model
attribute on the
+one side of the association is used to populate the records.
Using the User
and Pet
example lets look at how to build a schema where a User
may
+have many Pet
records and a Pet
may have multiple owners.
In this example, we will start with an array of users and an array of pets.
+We will create records for each element in each array then associate all of the Pets
with all
+of the Users
. If everything worked properly, we should be able to query any User
and see that
+they own all of the Pets
. Furthermore, we should be able to query any Pet
and see that
+it is owned by every User
.
./api/pet/models/Pet.settings.json
:
{
+ "attributes": {
+ "name": {
+ "type": "string",
+ "required": true,
+ "unique": true
+ },
+ "color": {
+ "type": "string",
+ "required": true
+ },
+ "owners": {
+ "collection": "user",
+ "via": "pets"
+ }
+ }
+}
+
./api/user/models/User.settings.json
:
{
+ "attributes": {
+ "name": {
+ "type": "string",
+ "required": true,
+ "unique": true
+ },
+ "age": {
+ "type": "integer",
+ "required": true
+ },
+ "pets": {
+ "collection": "pet",
+ "via": "owners"
+ }
+ }
+}
+
Lifecycle callbacks are functions you can define to run at certain times in a query. +They are hooks that you can tap into in order to change data.
Strapi exposes a handful of lifecycle callbacks by default.
beforeValidate
: fn(values, cb)
afterValidate
: fn(values, cb)
beforeCreate
: fn(values, cb)
afterCreate
: fn(newlyInsertedRecord, cb)
beforeValidate: fn(valuesToUpdate, cb)
afterValidate: fn(valuesToUpdate, cb)
beforeUpdate: fn(valuesToUpdate, cb)
afterUpdate: fn(updatedRecord, cb)
beforeDestroy
: fn(criteria, cb)
afterDestroy
: fn(deletedRecord, cb)
For example, this could be your ./api/pet/models/Pet.js
file:
module.exports = {
+ /**
+ * Basic settings
+ */
+
+ // The identity to use.
+ identity: settings.identity,
+
+ // The connection to use.
+ connection: settings.connection,
+
+ // Do you want to respect schema?
+ schema: settings.schema,
+
+ // Merge simple attributes from settings with those ones.
+ attributes: _.merge(settings.attributes, {
+
+ }),
+
+ // Do you automatically want to have time data?
+ autoCreatedAt: settings.autoCreatedAt,
+ autoUpdatedAt: settings.autoUpdatedAt,
+
+ /**
+ * Lifecycle callbacks on create
+ */
+
+ // Before creating a value.
+ beforeCreate: function (values, next) {
+ // Do some stuff
+ next();
+ },
+
+ // After creating a value.
+ afterCreate: function (newlyInsertedRecord, next) {
+ // Do some stuff
+ next();
+ },
+
+ /**
+ * Lifecycle callbacks on update
+ */
+
+ // Before updating a value.
+ beforeUpdate: function (valuesToUpdate, next) {
+ // Do some stuff
+ next();
+ },
+
+ // After updating a value.
+ afterUpdate: function (updatedRecord, next) {
+ // Do some stuff
+ next();
+ },
+
+ /**
+ * Lifecycle callbacks on destroy
+ */
+
+ // Before destroying a value.
+ beforeDestroy: function (criteria, next) {
+ // Do some stuff
+ next();
+ },
+
+ // After destroying a value.
+ afterDestroy: function (destroyedRecords, next) {
+ // Do some stuff
+ next();
+ }
+
+ ← + Internationalization + + Request + → +
The Waterline Query Interface allows you to interact with your models the same +way no matter which adapter they are using. This means you can use the same Query +Language whether your data lives in MySQL, MongoDB, PostgreSQL, etc.
Every model in Waterline will have a set of query methods exposed on it to allow
+you to interact with the database in a normalized fashion.
+These are known as the CRUD (Create
, Read
, Update
and Delete
) methods and
+is the primary way of interacting with your data.
There are also a special set of queries known as dynamic queries. +These are special class methods that are dynamically generated when you initialize Waterline. +We call them dynamic finders. They perform many of the same functions as the other class +methods but you can call them directly on an attribute in your model.
For most class methods, the callback parameter is optional and if one is not supplied, +it will return a chainable object.
find
will return an array of records that match the supplied criteria.
+Criteria can be built using the Query Language.
criteria
is required and accepts {}
, [{}]
, string
and int
data types.callback
function is optional.Any string arguments passed must be the ID of the record.
+This method will always return records in an array.
+If you are trying to find an attribute that is an array, you must wrap it in an additional
+set of brackets otherwise Waterline will think you want to perform an inQuery
.
User.find({
+ name: 'Walter Jr'
+ })
+ .exec(function (err, users) {
+ if (err) {
+ console.log(err);
+ }
+ console.log(users);
+ });
+
findOne
will return an object with the first matching result in the data store.
criteria
is required and accepts {}
, [{}]
, string
and int
data types.callback
function is optional.Any string arguments passed must be the ID of the record.
+If you are trying to find an attribute that is an array, you must wrap it in an additional
+set of brackets otherwise Waterline will think you want to perform an inQuery
.
User.findOne({
+ name: 'Walter Jr'
+ })
+ .exec(function (err, user) {
+ if (err) {
+ console.log(err);
+ }
+ console.log(user);
+ });
+
create
will attempt to create a new record in the datastore.
+If the data is valid and passes all validations it will be sent to the adapters create
method.
criteria
is required and accepts {}
and [{}]
data types.callback
function is optional.User.create({
+ name: 'Walter Jr'
+ })
+ .exec(function (err, user) {
+ if (err) {
+ console.log(err);
+ }
+ console.log(user);
+ });
+
findOrCreate
will return a single record if one was found or created,
+or an array of records if multiple get found/created via the supplied criteria or values.
+Criteria can be built using the Query Language.
criteria
is required and accepts {}
, [{}]
, string
and int
data types.values
is optional and accepts {}
and [{}]
data types.callback
function is optional.Any string arguments passed must be the ID of the record. +This method can return a single record or an array of records. +If a model is not found and creation values are omitted, it will get created with the supplied criteria values.
Unless an adapter implements its own version of findOrCreate
, findOrCreate
will do the
+find
and create
calls in two separate steps (not transactional).
+In a high frequency scenario it's possible for duplicates to be created if the query field(s) are not indexed.
Either user(s) with the name "Walter Jr" get returned or +a single user gets created with the name "Walter Jr" and returned:
User.findOrCreate({
+ name: 'Walter Jr'
+ })
+ .exec(function (err, users) {
+ if (err) {
+ console.log(err);
+ }
+ console.log(users);
+ });
+
update
will attempt to update any records matching the criteria passed in.
+Criteria can be built using the Query Language.
criteria
is required and accepts {}
, [{}]
, string
and int
data types.values
is required and accepts {}
and [{}]
data types.callback
function is optional.Although you may pass .update()
an object or an array of objects,
+it will always return an array of objects. Any string arguments passed must be the ID
+of the record. If you specify a primary key instead of a criteria object,
+any .where()
filters will be ignored.
User.update({
+ name: 'Walter Jr'
+ }, {
+ name: 'Flynn'
+ })
+ .exec(function (err, user) {
+ if (err) {
+ console.log(err);
+ }
+ console.log(user);
+ });
+
destroy
will destroy any records matching the provided criteria.
+Criteria can be built using the Query Language.
criteria
is required and accepts {}
, [{}]
, string
and int
data types.callback
function is optional.If you want to confirm the record exists before you delete it,
+you must first perform a .find()
. Any string arguments passed must be the ID of the record.
User.destroy({
+ name: 'Flynn'
+ })
+ .exec(function (err) {
+ if (err) {
+ console.log(err);
+ }
+ });
+
Some adapters, such as sails-mysql
and sails-postgresql
, support the query function
+which will run the provided RAW query against the database.
+This can sometimes be useful if you want to run complex queries and performance is very important.
query
is required and accepts string
data types.data
is optional and accepts array
data types.callback
function is required.The type of the results returned depend on your adapter: sails-mysql
returns an array of objects
+and sails-postgresql
returns an object containing metadata and the actual results within a 'rows' array.
+This function does currently not support promises.
Using PostgreSQL:
const title = "The King's Speech";
+Movie.query('SELECT * FROM movie WHERE title = $1', [title], function (err, results) {
+ console.log('Found the following movie: ', results.rows[0]);
+});
+
Using MySQL:
const title = "The King's Speech";
+Movie.query('SELECT * FROM movie WHERE title = $1', [title], function (err, results) {
+ console.log('Found the following movie: ', results[0]);
+});
+
The Waterline Query Language is an object based criteria used to retrieve the +records from any of the supported database adapters. +This allows you to change your database without changing your codebase.
All queries inside of Waterline are case insensitive. This allows for easier querying +but makes indexing strings tough. This is something to be aware of if you are +indexing and searching on string fields.
The criteria objects are formed using one of four types of object keys. +These are the top level keys used in a query object. It is loosely based on the +criteria used in MongoDB with a few slight variations.
Queries can be built using either a where
key to specify attributes,
+which will allow you to also use query options such as limit
and skip
or
+if where
is excluded the entire object will be treated as a where
criteria.
User.find({
+ where: {
+ name: 'John'
+ },
+ skip: 20,
+ limit: 10,
+ sort: 'name DESC'
+});
+
Or:
User.find({
+ name: 'John'
+});
+
A key pair can be used to search records for values matching exactly what is specified. +This is the base of a criteria object where the key represents an attribute on a model +and the value is a strict equality check of the records for matching values.
User.find({
+ name: 'John'
+});
+
They can be used together to search multiple attributes:
User.find({
+ name: 'John',
+ country: 'France'
+});
+
Modified pairs also have model attributes for keys but they also use any of the +supported criteria modifiers to perform queries where a strict equality check wouldn't work.
User.find({
+ name: {
+ contains: 'alt'
+ }
+})
+
In queries work similarly to MySQL in
queries. Each element in the array is treated as or
.
User.find({
+ name: ['John', 'Walter']
+});
+
Not-In queries work similar to in
queries, except for the nested object criteria.
User.find({
+ name: {
+ '!': ['John', 'Walter']
+ }
+});
+
Performing OR
queries is done by using an array of query pairs.
+Results will be returned that match any of the criteria objects inside the array.
User.find({
+ or: [
+ {
+ name: 'John'
+ },
+ {
+ occupation: 'Developer'
+ }
+ ]
+});
+
The following modifiers are available to use when building queries:
<
or lessThan
<=
or lessThanOrEqual
>
or greaterThan
>=
or greaterThanOrEqual
!
or not
like
contains
startsWith
endsWith
Searches for records where the value is less than the value specified.
User.find({
+ age: {
+ '<': 30
+ }
+});
+
Searches for records where the value is less or equal to the value specified.
User.find({
+ age: {
+ '<=': 21
+ }
+});
+
Searches for records where the value is more than the value specified.
User.find({
+ age: {
+ '>': 18
+ }
+});
+
Searches for records where the value is more or equal to the value specified.
User.find({
+ age: {
+ '>=': 21
+ }
+});
+
Searches for records where the value is not equal to the value specified.
User.find({
+ name: {
+ '!': 'John'
+ }
+});
+
Searches for records using pattern matching with the %
sign.
User.find({
+ food: {
+ 'like': '%burgers'
+ }
+});
+
A shorthand for pattern matching both sides of a string. +Will return records where the value contains the string anywhere inside of it.
User.find({
+ class: {
+ 'like': '%history%'
+ }
+});
+
User.find({
+ class: {
+ 'contains': 'history'
+ }
+});
+
A shorthand for pattern matching the right side of a string +Will return records where the value starts with the supplied string value.
User.find({
+ class: {
+ 'startsWith': 'french'
+ }
+});
+
User.find({
+ class: {
+ 'like': 'french%'
+ }
+});
+
A shorthand for pattern matching the left side of a string. +Will return records where the value ends with the supplied string value.
User.find({
+ class: {
+ 'endsWith': 'can'
+ }
+});
+
User.find({
+ class: {
+ 'like': '%can'
+ }
+});
+
You can do date range queries using the comparison operators.
User.find({
+ date: {
+ '>': new Date('2/4/2014'),
+ '<': new Date('2/7/2014')
+ }
+});
+
Query options allow you refine the results that are returned from a query.
Limit the number of results returned from a query.
User.find({
+ where: {
+ name: 'John'
+ },
+ limit: 20
+});
+
Return all the results excluding the number of items to skip.
User.find({
+ where: {
+ name: 'John'
+ },
+ skip: 10
+});
+
skip
and limit
can be used together to build up a pagination system.
User.find({
+ where: {
+ name: 'John'
+ },
+ limit: 10,
+ skip: 10
+});
+
Results can be sorted by attribute name. Simply specify an attribute name for
+natural (ascending) sort, or specify an asc
or desc
flag for ascending or
+descending orders respectively.
Sort by name in ascending order:
User.find({
+ where: {
+ name: 'John'
+ },
+ sort: 'name'
+});
+
Sort by name in descending order:
User.find({
+ where: {
+ name: 'John'
+ },
+ sort: 'name DESC'
+});
+
Sort by name in ascending order:
User.find({
+ where: {
+ name: 'John'
+ },
+ sort: 'name ASC'
+});
+
Sort by binary notation
User.find({
+ where: {
+ name: 'John'
+ },
+ sort: {
+ name: 1
+ }
+});
+
Sort by multiple attributes:
User.find({
+ where: {
+ name: 'John'
+ },
+ sort: {
+ name: 1,
+ age: 0
+ }
+});
+
Apply a projection to a Waterline query.
This example only returns the field name:
User.find({
+ where: {
+ age: {
+ '<': 30
+ }
+ },
+ select: ['name']
+});
+
+ ← + Introduction + + Response + → +
A Strapi Request
object is an abstraction on top of Node's vanilla request object,
+providing additional functionality that is useful for every day HTTP server
+development.
Request header object.
Request header object. Alias as request.header
.
Request method.
Set request method, useful for implementing middleware
+such as methodOverride()
.
Return request Content-Length as a number when present, or undefined
.
Get request URL.
Set request URL, useful for url rewrites.
Get request original URL.
Get origin of URL, include protocol
and host
.
this.request.origin
+// => http://example.com
+
Get full request URL, include protocol
, host
and url
.
this.request.href
+// => http://example.com/foo/bar?q=1
+
Get request pathname.
Set request pathname and retain query-string when present.
Get raw query string void of ?
.
Set raw query string.
Get raw query string with the ?
.
Set raw query string.
Get host (hostname:port) when present. Supports X-Forwarded-Host
+when strapi.app.proxy
is true
, otherwise Host
is used.
Get hostname when present. Supports X-Forwarded-Host
+when strapi.app.proxy
is true
, otherwise Host
is used.
Get request Content-Type
void of parameters such as "charset".
const ct = this.request.type;
+// => "image/png"
+
Get request charset when present, or undefined
:
this.request.charset
+// => "utf-8"
+
Get parsed query-string, returning an empty object when no +query-string is present. Note that this getter does not +support nested parsing.
For example "color=blue&size=small":
{
+ color: 'blue',
+ size: 'small'
+}
+
Set query-string to the given object. Note that this +setter does not support nested objects.
this.query = { next: '/login' };
+
Check if a request cache is "fresh", aka the contents have not changed. This
+method is for cache negotiation between If-None-Match
/ ETag
, and
+If-Modified-Since
and Last-Modified
. It should be referenced after setting
+one or more of these response headers.
// freshness check requires status 20x or 304
+this.status = 200;
+this.set('ETag', '123');
+
+// cache is ok
+if (this.fresh) {
+ this.status = 304;
+ return;
+}
+
+// cache is stale
+// fetch new data
+this.body = yield db.find('something');
+
Inverse of request.fresh
.
Return request protocol, "https" or "http". Supports X-Forwarded-Proto
+when strapi.app.proxy
is true
.
Shorthand for this.protocol == "https"
to check if a request was
+issued via TLS.
Request remote address. Supports X-Forwarded-For
when strapi.app.proxy
+is true
.
When X-Forwarded-For
is present and strapi.app.proxy
is enabled an array
+of these ips is returned, ordered from upstream -> downstream. When disabled
+an empty array is returned.
Return subdomains as an array.
Subdomains are the dot-separated parts of the host before the main domain of
+the app. By default, the domain of the app is assumed to be the last two
+parts of the host. This can be changed by setting strapi.app.subdomainOffset
.
For example, if the domain is "tobi.ferrets.example.com":
+If strapi.app.subdomainOffset
is not set, this.subdomains is ["ferrets", "tobi"]
.
+If strapi.app.subdomainOffset
is 3, this.subdomains is ["tobi"]
.
Check if the incoming request contains the "Content-Type"
+header field, and it contains any of the give mime type
s.
+If there is no request body, undefined
is returned.
+If there is no content type, or the match fails false
is returned.
+Otherwise, it returns the matching content-type.
// With Content-Type: text/html; charset=utf-8
+this.is('html'); // => 'html'
+this.is('text/html'); // => 'text/html'
+this.is('text/*', 'text/html'); // => 'text/html'
+
+// When Content-Type is application/json
+this.is('json', 'urlencoded'); // => 'json'
+this.is('application/json'); // => 'application/json'
+this.is('html', 'application/*'); // => 'application/json'
+
+this.is('html'); // => false
+
For example if you want to ensure that +only images are sent to a given route:
if (this.is('image/*')) {
+ // process
+} else {
+ this.throw(415, 'images only!');
+}
+
Strapi's request
object includes helpful content negotiation utilities powered by
+accepts and
+negotiator.
These utilities are:
request.accepts(types)
request.acceptsEncodings(types)
request.acceptsCharsets(charsets)
request.acceptsLanguages(langs)
If no types are supplied, all acceptable types are returned.
If multiple types are supplied, the best match will be returned. If no matches are found,
+a false
is returned, and you should send a 406 "Not Acceptable"
response to the client.
In the case of missing accept headers where any type is acceptable, the first type will +be returned. Thus, the order of types you supply is important.
Check if the given type(s)
is acceptable, returning the best match when true, otherwise
+false
. The type
value may be one or more mime type string
+such as "application/json", the extension name
+such as "json", or an array ["json", "html", "text/plain"]
.
// Accept: text/html
+this.accepts('html');
+// => "html"
+
+// Accept: text/*, application/json
+this.accepts('html');
+// => "html"
+this.accepts('text/html');
+// => "text/html"
+this.accepts('json', 'text');
+// => "json"
+this.accepts('application/json');
+// => "application/json"
+
+// Accept: text/*, application/json
+this.accepts('image/png');
+this.accepts('png');
+// => false
+
+// Accept: text/*;q=.5, application/json
+this.accepts(['html', 'json']);
+this.accepts('html', 'json');
+// => "json"
+
+// No Accept header
+this.accepts('html', 'json');
+// => "html"
+this.accepts('json', 'html');
+// => "json"
+
You may call this.accepts()
as many times as you like,
+or use a switch:
switch (this.accepts('json', 'html', 'text')) {
+ case 'json': break;
+ case 'html': break;
+ case 'text': break;
+ default: this.throw(406, 'json, html, or text only');
+}
+
Check if encodings
are acceptable, returning the best match when true, otherwise false
.
+Note that you should include identity
as one of the encodings!
// Accept-Encoding: gzip
+this.acceptsEncodings('gzip', 'deflate', 'identity');
+// => "gzip"
+
+this.acceptsEncodings(['gzip', 'deflate', 'identity']);
+// => "gzip"
+
When no arguments are given all accepted encodings +are returned as an array:
// Accept-Encoding: gzip, deflate
+this.acceptsEncodings();
+// => ["gzip", "deflate", "identity"]
+
Note that the identity
encoding (which means no encoding) could be unacceptable if
+the client explicitly sends identity;q=0
. Although this is an edge case, you should
+still handle the case where this method returns false
.
Check if charsets
are acceptable, returning
+the best match when true, otherwise false
.
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
+this.acceptsCharsets('utf-8', 'utf-7');
+// => "utf-8"
+
+this.acceptsCharsets(['utf-7', 'utf-8']);
+// => "utf-8"
+
When no arguments are given all accepted charsets +are returned as an array:
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
+this.acceptsCharsets();
+// => ["utf-8", "utf-7", "iso-8859-1"]
+
Check if langs
are acceptable, returning
+the best match when true, otherwise false
.
// Accept-Language: en;q=0.8, es, pt
+this.acceptsLanguages('es', 'en');
+// => "es"
+
+this.acceptsLanguages(['en', 'es']);
+// => "es"
+
When no arguments are given all accepted languages +are returned as an array:
// Accept-Language: en;q=0.8, es, pt
+this.acceptsLanguages();
+// => ["es", "pt", "en"]
+
Check if the request is idempotent.
Return the request socket.
Return request header.
+ ← + Models + + Services + → +
A Strapi Response
object is an abstraction on top of Node's vanilla response object,
+providing additional functionality that is useful for every day HTTP server
+development.
Response header object.
Response header object. Alias as response.header
.
Request socket.
Get response status. By default, response.status
is not set unlike Node's
+res.statusCode
which defaults to 200
.
Set response status via numeric code:
Don't worry too much about memorizing these strings, if you have a typo an error will be thrown, +displaying this list so you can make a correction.
Get response status message. By default, response.message
is
+associated with response.status
.
Set response status message to the given value.
Set response Content-Length to the given value.
Return response Content-Length as a number when present, or deduce
+from this.body
when possible, or undefined
.
Get response body.
Set response body to one of the following:
string
writtenBuffer
writtenStream
pipedObject
json-stringifiednull
no content responseIf response.status
has not been set, Strapi will automatically set the status to 200
or 204
.
The Content-Type is defaulted to text/html or text/plain, both with +a default charset of utf-8. The Content-Length field is also set.
The Content-Type is defaulted to application/octet-stream, and Content-Length +is also set.
The Content-Type is defaulted to application/octet-stream.
The Content-Type is defaulted to application/json.
Get a response header field value with case-insensitive field
.
const etag = this.get('ETag');
+
Set response header field
to value
:
this.set('Cache-Control', 'no-cache');
+
Append additional header field
with value val
.
this.append('Link', '<http://127.0.0.1/>');
+
Set several response header fields
with an object:
this.set({
+ 'Etag': '1234',
+ 'Last-Modified': date
+});
+
Remove header field
.
Get response Content-Type
void of parameters such as "charset".
const ct = this.type;
+// => "image/png"
+
Set response Content-Type
via mime string or file extension.
this.type = 'text/plain; charset=utf-8';
+this.type = 'image/png';
+this.type = '.png';
+this.type = 'png';
+
Note: when appropriate a charset
is selected for you, for
+example response.type = 'html'
will default to "utf-8", however
+when explicitly defined in full as response.type = 'text/html'
+no charset is assigned.
Very similar to this.request.is()
.
+Check whether the response type is one of the supplied types.
+This is particularly useful for creating middleware that
+manipulate responses.
For example, this is a middleware that minifies +all HTML responses except for streams.
const minify = require('html-minifier');
+
+strapi.app.use(function *minifyHTML(next) {
+ yield next;
+
+ if (!this.response.is('html')) {
+ return;
+ }
+
+ const body = this.body;
+ if (!body || body.pipe) {
+ return;
+ }
+
+ if (Buffer.isBuffer(body)) {
+ body = body.toString();
+ }
+
+ this.body = minify(body);
+});
+
Perform a [302] redirect to url
.
The string "back" is special-cased
+to provide Referrer support, when Referrer
+is not present alt
or "/" is used.
this.redirect('back');
+this.redirect('back', '/index.html');
+this.redirect('/login');
+this.redirect('http://google.com');
+
To alter the default status of 302
, simply assign the status
+before or after this call. To alter the body, assign it after this call:
this.status = 301;
+this.redirect('/cart');
+this.body = 'Redirecting to shopping cart';
+
Set Content-Disposition
to "attachment" to signal the client
+to prompt for download. Optionally specify the filename
of the
+download.
Check if a response header has already been sent. Useful for seeing +if the client may be notified on error.
Return the Last-Modified
header as a Date
, if it exists.
Set the Last-Modified
header as an appropriate UTC string.
+You can either set it as a Date
or date string.
this.response.lastModified = new Date();
+
Set the ETag of a response including the wrapped "
s.
+Note that there is no corresponding response.etag
getter.
this.response.etag = crypto.createHash('md5').update(this.body).digest('hex');
+
Vary on field
.
+ ← + Query Interface + + Sessions + → +
The most basic feature of any web application is the ability to interpret a request sent to a URL, +then send back a response. In order to do this, your application has to be able to distinguish one URL +from another.
Like most web frameworks, Strapi provides a router: a mechanism for mapping URLs to controllers. +Routes are rules that tell Strapi what to do when faced with an incoming request.
Routes can be found in ./api/<apiName>/config/routes.json
.
Each route consists of an address (as a key) and a target (as an object value).
+The address is a URL path and a specific HTTP method. The target is defined by an object with a
+controller
and an action
. When the router receives an incoming request, it checks the address
+of all routes for matches. If a matching route is found, the request is then passed to its target.
{
+ "routes": {
+ "VERB /endpoint/:param": {
+ "controller": "controllerName",
+ "action": "actionName"
+ }
+ }
+ }
+
For example to manage your Post
records with a CRUD, your route should look like this:
{
+ "routes": {
+ "GET /post": {
+ "controller": "Post",
+ "action": "find"
+ }
+ "GET /post/:id": {
+ "controller": "Post",
+ "action": "findOne"
+ },
+ "POST /post": {
+ "controller": "Post",
+ "action": "create"
+ },
+ "PUT /post/:id": {
+ "controller": "Post",
+ "action": "update"
+ },
+ "DELETE /post/:id": {
+ "controller": "Post",
+ "action": "delete"
+ }
+ }
+ }
+
Route paths will be translated to regular expressions used to match requests. +Query strings will not be considered when matching requests.
Route parameters are captured and added to ctx.params
or ctx.request.body
.
By taking the previous example, your Post
controller should look like this:
module.exports = {
+
+ // GET request
+ find: function *() {
+ try {
+ this.body = yield Post.find(this.params);
+ } catch (error) {
+ this.body = error;
+ }
+ },
+
+ findOne: function *() {
+ try {
+ this.body = yield Post.findOne(this.params);
+ } catch (error) {
+ this.body = error;
+ }
+ },
+
+ // POST request
+ create: function *() {
+ try {
+ this.body = yield Post.create(this.request.body);
+ } catch (error) {
+ this.body = error;
+ }
+ },
+
+ // PUT request
+ update: function *() {
+ try {
+ this.body = yield Post.update(this.params.id, this.request.body);
+ } catch (error) {
+ this.body = error;
+ }
+ },
+
+ // DELETE request
+ delete: function *() {
+ try {
+ this.body = yield Post.destroy(this.params);
+ } catch (error) {
+ this.body = error;
+ }
+ }
+};
+
+
Keep in mind routes can automatically be prefixed in ./config/general.json
with the prefix
key.
+Let an empty string if you don't want to prefix your API. The prefix must starts with a /
, e.g. /api
.
Just because a request matches a route address doesn't necessarily mean it will be passed to that +route's target directly. The request will need to pass through any configured policies first. +Policies are versatile tools for authorization and access control. They let you allow or deny +access to your controllers down to a fine level of granularity.
Policies are defined in the policies
directory of every of your APIs.
Each policy file should contain a single function. When it comes down to it, policies are +really just functions which run before your controllers. You can chain as many of them +together as you like. In fact they're designed to be used this way. Ideally, each middleware +function should really check just one thing.
For example to access DELETE /post/:id
, the request will go through the isAdmin
policy first.
+If the policy allows the request, then the delete
action from the Post
controller is executed.
{
+ "routes": {
+ "DELETE /post/:id": {
+ "controller": "Post",
+ "action": "delete",
+ "policies": ["isAdmin"]
+ }
+ }
+ }
+
Do not forget to yield next
when you need to move on.
+ ← + Logging + + Upload + → +
Services can be thought of as libraries which contain functions that you might want to use
+in many places of your application. For example, you might have an Email
service which
+wraps some default email message boilerplate code that you would want to use in many parts
+of your application.
Simply create a JavaScript file containing a function or an object into your
+./api/<apiName>/services
directory.
For example, you could have an Email service
like this:
const nodemailer = require('nodemailer');
+
+module.exports = {
+ sendEmail: function (from, to, subject, text) {
+
+ // Create reusable transporter object using SMTP transport
+ const transporter = nodemailer.createTransport({
+ service: 'Gmail',
+ auth: {
+ user: 'gmail.user@gmail.com',
+ pass: 'userpass'
+ }
+ });
+
+ // Setup e-mail data
+ const options = {
+ from: from,
+ to: to,
+ subject: subject,
+ text: text
+ };
+
+ // Send mail
+ transporter.sendMail(options, function(error, info){
+ if (error) {
+ console.log(error);
+ return false;
+ }
+
+ console.log('Message sent: ' + info.response);
+ });
+ }
+};
+
+ ← + Request + + Users + → +
Since HTTP driven applications are stateless, sessions provide a way to store information +about the user across requests.
Strapi provides "guest" sessions, meaning any visitor will have a session,
+authenticated or not. If a session is new a Set-Cookie
will be produced regardless
+of populating the session.
Strapi only supports cookie sessions, for now.
The current session is available in this.session
inside a controller action.
module.exports = {
+ find: function *() {
+
+ // Limit request rate to 100
+ if (this.session.views < 100) {
+ try {
+ this.session.views++;
+ this.body = yield Post.find(this.params);
+ } catch (error) {
+ this.body = error;
+ }
+ } else {
+ this.body = 'You have reached your request rate limit';
+ }
+ }
+};
+
To destroy an active session, simply set it to null
:
module.exports = {
+ logout: function () {
+ try {
+ this.session = null;
+ this.redirect('./');
+ } catch (error) {
+ this.body = error;
+ }
+ }
+};
+
+ ← + Response + + Testing + → +
Strapi's test suite is written using mocha and although +Strapi doesn't impose any testing framework for your apps, in this example we +will setup tests using the mocha framework.
Before writing tests, you should setup a basic directory structure, like this:
./strapiApp
+├── api/
+├── ...
+├── test/
+│ ├── integration/
+│ │ ├── controllers/
+│ │ │ └── my_endpoint.test.js
+│ │ ├── models/
+│ │ │ └── my_model.test.js
+│ │ └── ...
+| ├── ...
+│ ├── bootstrap.js
+
We are going to setup a bootstrap.js
with before
and after
hooks to
+perform any actions before and after our tests.
+In this example, the app server is started before running any tests an stop
+the server after tests are completed.
./test/bootstrap.js
const strapi = require('strapi');
+
+before(function (done) {
+ strapi.start({}, function(err) {
+ if (err) {
+ return done(err);
+ }
+
+ done(err, strapi);
+ });
+});
+
+after(function (done) {
+ strapi.stop(done());
+});
+
Once you have setup your directory structure, you can start writing your tests.
+In this example we use co-supertest,
+a co
and Supertest
integration library.
+Supertest provides several useful
+methods for testing HTTP requests.
+If you want to test an api endpoint, you can do it like this:
./test/integration/controllers/my_endpoint.js
const request = require('co-supertest');
+
+describe('MyEndpoint Controller Integration', function() {
+ describe('GET /my_endpoint', function() {
+ it('should return 200 status code', function *() {
+ yield request(strapi.config.url)
+ .get('/my_endpoint')
+ .expect(200)
+ .expect('Content-Type', /json/)
+ .end();
+ });
+ });
+});
+
In order to run tests you can use npm test
. In your package.json
, in the
+scripts
section, add this:
./package.json
"scripts": {
+ "test": "mocha --require co-mocha test/bootstrap.js test/**/*.test.js"
+}
+
Remember to run test/bootstrap.js
before any other tests and, if you want,
+use the --require
option to pass any required dependencies you need available
+in your tests.
+ ← + Sessions + + Views + → +
Strapi contains a set of tools to upload files.
To change the upload config, edit the ./api/upload/config/settings.json
file.
For the config bellow, please use refer to the [co-busboy](https://github.com/cojs/busboy)
node module documentation.
{
+ "upload": {
+ "folder": "public/upload",
+ "acceptedExtensions": [
+ "*"
+ ],
+ "headers": {},
+ "highWaterMark": "",
+ "fileHwm": "",
+ "defCharset": "",
+ "preservePath": "",
+ "limits": {
+ "fieldNameSize": "",
+ "fieldSize": "",
+ "fields": "",
+ "fileSize": "",
+ "files": "",
+ "parts": "",
+ "headerPairs": ""
+ }
+ }
+}
+
The upload service allows you to easily upload files from anywhere in your application.
Usage as a promise (yieldable) :
yield strapi.api.upload.services.upload.upload(part, this);
+
The upload API is a simple API which can be used from your client +(front-end, mobile...) application to upload files.
Route used to upload files:
POST /upload
+
To use this route, you have to submit a HTML form with multipart/*
enctype
+(or fake it if you are using a web front-end framework like AngularJS).
Response payload:
[
+ {
+ "readable": true,
+ "domain": null,
+ "truncated": false,
+ "fieldname": "file",
+ "filename": "1445421755771-image.jpg",
+ "encoding": "7bit",
+ "transferEncoding": "7bit",
+ "mime": "image/jpeg",
+ "mimeType": "image/jpeg",
+ "originalFilenameFormatted": "image.jpg",
+ "originalFilename": "image.jpg",
+ "template": "default",
+ "lang": "en",
+ "createdAt": "2015-10-21T10:02:35.776Z",
+ "updatedAt": "2015-10-21T10:02:35.776Z",
+ "id": 2
+ }
+]
+
Each uploaded file description is registered in the database. So you can retrieve +them whenever you want. However, you can disable this option by overriding the +upload service logic.
+ ← + Router + + CLI + → +
Most of the web applications require a user management system: registration, login, +reset password, etc.
To avoid you to reinvent the wheel, Strapi embedded a full featured user management +system powered by Grant and JSON Web Token (JWT).
Route used to register a user to your application: POST /auth/local/register
.
Request payload:
{
+ "username": "John DOE",
+ "email": "contact@company.com",
+ "password": "123456"
+}
+
Response payload:
{
+ "user": {},
+ "jwt": ""
+}
+
Route used to login a user to your application: POST /auth/local
.
Request payload:
{
+ "identifier": "contact@company.com",
+ "password": "123456"
+}
+
Response payload:
{
+ "user": {},
+ "jwt": ""
+}
+
JWT does not use session. Once you get the token, it has to be stored in front (for
+example in the localstorage
), and sent within each request. The token can be sent:
Bearer
)token
field)token
field)Thanks to Grant and Purest, you can easily use OAuth and OAuth2 +providers to enable authentication in your application. By default, +Strapi comes with four providers:
To use the providers authentication, set your credentials in
+./api/user/config/environments/development/grant.json
.
Redirect your user to: GET /connect/:provider
.
After his approval, he will be redirected to /auth/:provider/callback
. The jwt and user will be available in the querystring.
Response payload:
{
+ "user": {},
+ "jwt": ""
+}
+
Strapi comes with 5 providers. If you want to add another one, it can be easily done thanks to Purest, by adding it in the Grant service.
Send an email to the user with an activation code: POST /auth/forgot-password
.
Request payload:
{
+ "email": "contact@company.com"
+}
+
Route used to update the password of a user after he asked for a
+"forgot-password" email: POST /auth/change-password
.
Request payload:
{
+ "code": "",
+ "password": "123456",
+ "passwordConfirmation": "123456"
+}
+
Response payload:
{
+ "user": {},
+ "jwt": ""
+}
+
If you want to access attributes of the logged in user, you can use this.user
inside of your controller action.
+ ← + Services +
In Strapi, views are markup templates that are compiled on the server into HTML pages. +In most cases, views are used as the response to an incoming HTTP request.
By default, Strapi doesn't use views. The philosophy of the framework is to +separate the reusable backend application logic from the frontend.
If you want to activate views, set the views
in ./config/general.json
.
For example, if you want to use lodash
for .html
files and use it by default,
+you may set up your views
object as below:
{
+ "views": {
+ "map": {
+ "html": "lodash"
+ },
+ "default": "html"
+ }
+}
+
Views are defined in your application's ./views
directory.
Simply use this.render
instead of this.body
to render a view.
You don't need to specify the view extension if you use the default one sets in config.
Using the config we wrote above with lodash
for .html
files and use the html
+extension by default, this example will render ./views/user.html
with
+Lodash as template engine.
yield this.render('user', {
+ firstname: 'John',
+ lastname: 'Doe'
+});
+
<html>
+ <head>...</head>
+ <body>
+ <p>Firstname: <% firstname %><br>Lastname: <% lastname %></p>
+ </body>
+</html>
+
Here is the same example with the jade
extension, not used by default:
yield this.render('user.jade', {
+ firstname: 'John',
+ lastname: 'Doe'
+});
+
To use a view engine, you should use npm to install it in your project and
+set the map
object in strapi.config.views
. For example, if you want to use
+swig
for .html
files and hogan
for .md
files, you may configure the
+map
object as below:
{
+ "views": {
+ "map": {
+ "html": "swig",
+ "md": "hogan"
+ }
+ }
+}
+
Strapi supports all of those view engines:
+ ← + Testing + + Blueprints + → +
One of Strapi's main feature is its fully extendable and customizable admin panel. This section explains how the admin panel section is structured and how to customize it.
See the Contributing Guide for informations on how to develop the Strapi's admin interface.
The entire logic of the admin panel is located in a single folder named ./admin/
. This directory contains the following structure:
/admin
+└─── admin
+| └─── build // Webpack generated build of the front-end
+| └─── src // Front-end directory
+| └─── app.js // Entry point of the React application
+| └─── assets // Assets directory containing images,...
+| └─── components // Admin's React components directory
+| └─── containers // Admin's high level components directory
+| └─── favicon.ico // Favicon displayed in web browser
+| └─── i18n.js // Internalization logic
+| └─── index.html // Basic html file in which are injected necessary styles and scripts
+| └─── reducers.js // Redux reducers logic
+| └─── store.js // Redux store logic
+| └─── styles // Directory containing the global styles. Specific styles are defined at the component level
+| └─── translations // Directory containing text messages for each supported languages
+└─── config
+| └─── routes.json // Admin's API routes
+| └─── layout.json // Admin's specific settings
+└─── controllers // Admin's API controllers
+└─── services // Admin's API services
+└─── packages.json // Admin's npm dependencies
+
The administration panel can be customised according to your needs, so you can make it reflects your identity: colors, fonts, logo, etc.
By default, the administration panel is exposed via http://localhost:1337/admin. However, for security reasons, you can easily update this path.
Path — ./config/environment/**/server.json
.
{
+ "host": "localhost",
+ "port": 1337,
+ "autoReload": {
+ "enabled": true
+ },
+ "cron": {
+ "enabled": false
+ },
+ "admin": {
+ "path": "/dashboard"
+ }
+}
+
The panel will be available through http://localhost:1337/dashboard with the configurations above.
Note that to modify the administration panel, your project needs to be created with using the dev
flag, an example of such would be: strapi new strapi --dev
.
#1 — Install its own dependencies
Run npm install
from the ./admin
folder.
#2 — Launch the development server
Run npm start
from the ./admin
folder. That's all.
#3 — Go to the browser
You should be able to see the admin at http://localhost:4000/admin.
In development, all the plugins of your app are mounted in the same build as the administration panel.
Admin's styles use PostCSS, and more precisely PostCSS-SCSS. In this way, colors are stored in variables. The values of these variables can be easily changed in files located in ./admin/admin/src/styles/variables/
.
The changes should be automatically visible.
Fonts can also be overridden:
./admin/admin/src/styles/fonts
../admin/admin/src/styles/base/fonts.scss
../admin/admin/src/styles/variables/variables.bootstrap.scss
.To change the top-left displayed admin panel's logo, replace the image located at ./admin/admin/src/assets/images/logo-strapi.png
.
make sure the size of your image is the same as the existing one (434px x 120px).
To build the administration, run the following command from the root directory of your project.
npm run setup
+
This will replace the folder's content located at ./admin/admin/build
. Visit http://localhost:1337/admin/ to make sure your updates have been taken in account.
After you have built the admininistration you can now create a new project to develop your API with the changes implemented.
You should now create a project without --dev
The administration is nothing more than a React front-end application calling an API. The front-end and the back-end are independent and can be deployed on different servers which brings us to different scenarios:
Let's dive into the build configurations for each case.
You don't need to touch anything in your configuration file. This is the default behaviour and the build configurations will be automatically set. The server will start on the defined port and the administration panel will be accessible through http://yourdomain.com:1337/dashboard.
You might want to change the path to access to the administration panel. Here the required configurations to change the path:
Path — ./config/environment/**/server.json
.
{
+ "host": "localhost",
+ "port": 1337,
+ "autoReload": {
+ "enabled": false
+ },
+ "cron": {
+ "enabled": false
+ },
+ "admin": {
+ "path": "/dashboard" // We change the path to access to the admin (highly recommended for security reasons).
+ }
+}
+
You have to rebuild the administration panel to make this work. Please follow the step #2 of the deployment guide.
It's very common to deploy the front-end and the back-end on different servers. Here the required configurations to handle this case:
Path — ./config/environment/**/server.json
.
{
+ "host": "localhost",
+ "port": 1337,
+ "autoReload": {
+ "enabled": false
+ },
+ "cron": {
+ "enabled": false
+ },
+ "admin": {
+ "path": "/dashboard",
+ "build": {
+ "host": "/", // Note: The administration will be accessible from the root of the domain (ex: http//yourfrontend.com/)
+ "backend": "http://yourbackend.com",
+ "plugins": {
+ "source": "backend" // What does it means? The script tags in the index.html will use the backend value to load the plugins (ex: http://yourbackend.com/dashboard/content-manager/main.js).
+ }
+ }
+ }
+}
+
The administration URL will be http://yourfrontend.com and every request from the panel will hit the backend at http://yourbackend.com. The plugins will be injected through the origin
(means the API itself). In other words, the plugins URLs will be http://yourbackend.com/dashboard/content-manager/main.js
.
How it is possible? The API (the Strapi server) owns the plugin and these plugins are exposed through http://yourbackend.com/admin/**/main.js
The DOM should look like this:
Path — ./admin/admin/build/index.html
.
<html>
+ <head></head>
+ <body>
+ <div id="app"></div>
+ <script type="text/javascript" src="/vendor.dll.js"></script>
+ <script type="text/javascript" src="/main.js"></script>
+ <script src="http://yourbackend.com/dashboard/content-manager/main.js"></script>
+ <script src="http://yourbackend.com/dashboard/settings-manager/main.js"></script>
+ <script src="http://yourbackend.com/dashboard/content-type-builder/main.js"></script>
+ </body>
+</html>
+
The plugins are injected using the ./admin/admin/build/config/plugins.json
. To see the plugins URLs in the index.html
, you need to launch the administration panel in the browser.
In this case, we suppose that you decided to put your administration and the plugins on the same server but on a different server as the API.
Path — ./config/environment/**/server.json
.
{
+ "host": "localhost",
+ "port": 1337,
+ "autoReload": {
+ "enabled": false
+ },
+ "cron": {
+ "enabled": false
+ },
+ "admin": {
+ "build": {
+ "host": "http://yourfrontend.com/dashboard", // Note: The custom path has moved directly in the host URL.
+ "backend": "http://yourbackend.com",
+ "plugins": {
+ "source": "host", // What does it means? The script tags in the index.html will use the host value to load the plugins (ex: http://yourfrontend.com/dashboard/plugins/content-manager/main.js).
+ "folder": "/plugins"
+ }
+ }
+ }
+}
+
The administration URL will be http://yourfrontend.com/dashboard and every request from the panel will hit the backend at http://yourbackend.com. The plugins will be injected through the host
. It means that the plugins URLs will use the host URL as the origin. So the plugins URLs will be http://yourfrontend.com/dashboard/plugins/content-manager/main.js
.
We also added a folder
setting to separate the plugins from the administration build. In your server, the files structure should look like this:
- src/
+ - 0bd35bad03d09ca61ac6cce225112e36.svg
+ - 1d51d8767683a24635702f720cda4e26.svg
+ - af3aefd0529349e40e4817c87c620836.png
+ - config/
+ - plugins.json
+ - main.js
+ - main.js.map
+ - plugins/
+ - content-type-builder/
+ - 0bd35bad03d09ca61ac6cce225112e36.svg
+ - 1d51d8767683a24635702f720cda4e26.svg
+ - af3aefd0529349e40e4817c87c620836.png
+ - main.js
+ - main.js.map
+ - content-manager/
+ - ...
+ - main.js
+ - main.js.map
+ - settings-manager/
+ - ...
+ - main.js
+ - main.js.map
+ - vendor.dll.js
+ - vendor.dll.js.map
+
The generated index.html
will look like this:
Path — ./admin/admin/build/index.html
.
<html>
+ <head></head>
+ <body>
+ <div id="app"></div>
+ <script type="text/javascript" src="/dashboard/vendor.dll.js"></script>
+ <script type="text/javascript" src="/dashboard/main.js"></script>
+ <script src="/dashboard/plugins/content-manager/main.js"></script>
+ <script src="/dashboard/plugins/settings-manager/main.js"></script>
+ <script src="/dashboard/plugins/content-type-builder/main.js"></script>
+ </body>
+</html>
+
The plugins are injected using the ./admin/admin/build/config/plugins.json
. To see the plugins URLs in the index.html
, you need to launch the administration panel in the browser.
+ ← + Request + + Hooks + → +
The hooks are modules that add functionality to the core. They are loaded during the server boot. For example, if your project needs to work with a SQL database, your will have to add the hook strapi-hook-bookshelf
to be able to connect your app with your database.
Path — ./hooks/documentation/lib/index.js
.
const fs = require('fs');
+const path = require('path');
+
+module.exports = strapi => {
+ const hook = {
+
+ /**
+ * Default options
+ */
+
+ defaults: {
+ documentation: {
+ path: '/public/documentation'
+ }
+ },
+
+ /**
+ * Initialize the hook
+ */
+
+ initialize: cb => {
+ try {
+ // Check if documentation folder exist.
+ fs.accessSync(path.resolve(process.cwd(), this.defaults.documentation.path));
+ } catch (e) {
+ // Otherwise, create the folder.
+ fs.mkdirSync(path.resolve(process.cwd(), this.defaults.documentation.path));
+ }
+
+ // This function doesn't really exist,
+ // it's just an example to tell you that you
+ // run your business logic and when it's done
+ // you just need to call the callback `cb`
+ generateDocumentation(path.resolve(process.cwd(), this.defaults.documentation.path), function(err) {
+ if (err) {
+ // Error: it will display the error to the user
+ // and the hook won't be loaded.
+ return cb(err);
+ }
+
+ // Success.
+ cb();
+ });
+ }
+ };
+
+ return hook;
+};
+
defaults
(object): Contains the defaults configurations. This object is merged to strapi.config.hook.settings.**
.initialize
(function): Called during the server boot. The callback cb
needs to be called. Otherwise, the hook won't be loaded.Every folder that follows this name pattern strapi-*
in your ./node_modules
folder will be loaded as a hook. The hooks are accessible through the strapi.hook
variable.
A hook needs to follow the structure below:
/hook
+└─── lib
+ - index.js
+- LICENSE.md
+- package.json
+- README.md
+
The index.js
is the entry point to your hook. It should look like the example above.
It happens that a hook has a dependency to another one. For example, the strapi-hook-bookshelf
has a dependency to strapi-hook-knex
. Without it, the strapi-hook-bookshelf
can't work correctly. It also means that the strapi-hook-knex
hook has to be loaded before.
To handle this case, you need to update the package.json
at the root of your hook.
{
+ "name": "strapi-hook-bookshelf",
+ "version": "x.x.x",
+ "description": "Bookshelf hook for the Strapi framework",
+ "dependencies": {
+ ...
+ },
+ "strapi": {
+ "dependencies": [
+ "strapi-hook-knex"
+ ]
+ }
+}
+
The framework allows to load hooks from the project directly without having to install them from npm. It's great way to take advantage of the features of the hooks system for code that doesn't need to be shared between apps. To achieve this, you have to create a ./hooks
folder at the root of your project and put the hooks into it.
/project
+└─── admin
+└─── api
+└─── config
+└─── hooks
+│ └─── strapi-documentation
+│ - index.js
+│ └─── strapi-server-side-rendering
+│ - index.js
+└─── plugins
+└─── public
+- favicon.ico
+- package.json
+- server.js
+
+ ← + Admin panel + + Logging + → +
Strapi relies on an extremely fast Node.js logger called Pino that includes a shell utility to pretty-print its log files. It provides great performances and doesn't slow down your app. The logger is accessible through the global variable strapi.log
or the request's context ctx.log
if enabled.
// Folder.js controller
+const fs = require('fs');
+const path = require('path');
+
+module.exports = {
+
+ /**
+ * Retrieve app's folders.
+ *
+ * @return {Object|Array}
+ */
+
+ findFolders: async (ctx) => {
+ try {
+ const folders = fs.readdirSync(path.resolve(process.cwd()));
+
+ strapi.log.info(folders); // ctx.log.info(folders);
+
+ ctx.send(folders);
+ } catch (error) {
+ strapi.log.fatal(error); // ctx.log.fatal(error);
+ ctx.badImplementation(error.message);
+ }
+ }
+}
+
The global logger is configured by environment variables.
STRAPI_LOG_LEVEL
: Can be 'fatal', 'error', 'warn', 'info', 'debug' or 'trace'.
+STRAPI_LOG_TIMESTAMP
: Can be true/false
+STRAPI_LOG_PRETTY_PRINT
: Can be true/false
+STRAPI_LOG_FORCE_COLOR
: Can be true/false
To configure the request-logger middleware, you have to edit the following file ./config/environments/*/request.json
.
{
+ ...
+ "logger": {
+ "level": "debug",
+ "exposeInContext": true,
+ "requests": true
+ },
+ ...
+}
+
level
: defines the desired logging level (fatal, error, warn, info, debug, trace).exposeInContext
: allows access to the logger through the context.requests
: incoming HTTP requests will be logged.To find more details about the logger API, please refer to the Pino documentation.
+ ← + Hooks + + Middlewares + → +
The middlewares are functions which are composed and executed in a stack-like manner upon request. If you are not familiar with the middleware stack in Koa, we highly recommend you to read the Koa's documentation introduction.
Enable the middleware in environments settings
Path — [config/environments/**
]
"urlReader": {
+ "enabled": true
+ }
+
Path — strapi/lib/middlewares/responseTime/index.js
.
module.exports = strapi => {
+ return {
+ initialize: function(cb) {
+ strapi.app.use(async (ctx, next) => {
+ const start = Date.now();
+
+ await next();
+
+ const delta = Math.ceil(Date.now() - start);
+
+ // Set X-Response-Time header
+ ctx.set('X-Response-Time', delta + 'ms');
+ });
+
+ cb();
+ }
+ };
+};
+
initialize
(function): Called during the server boot. The callback cb
needs to be called. Otherwise, the middleware won't be loaded into the stack.The core of Strapi embraces a small list of middlewares for performances, security and great error handling.
The following middlewares cannot be disabled: responses, router, logger and boom.
A middleware needs to follow the structure below:
/middleware
+└─── lib
+ - index.js
+- LICENSE.md
+- package.json
+- README.md
+
The index.js
is the entry point to your middleware. It should look like the example above.
The framework allows the application to override the default middlewares and add new ones. You have to create a ./middlewares
folder at the root of your project and put the middlewares into it.
/project
+└─── admin
+└─── api
+└─── config
+└─── middlewares
+│ └─── responseTime // It will override the core default responseTime middleware
+│ - index.js
+│ └─── views // It will be added into the stack of middleware
+│ - index.js
+└─── plugins
+└─── public
+- favicon.ico
+- package.json
+- server.js
+
Every middleware will be injected into the Koa stack. To manage the load order, please refer to the Middleware order section.
The middlewares are injected into the Koa stack asynchronously. Sometimes it happens that some of these middlewares need to be loaded in a specific order. To define a load order, we created a dedicated file located in ./config/middleware.json
.
Path — ./config/middleware.json
.
{
+ "timeout": 100,
+ "load": {
+ "before": [
+ "responseTime",
+ "logger",
+ "cors",
+ "responses"
+ ],
+ "order": [
+ "Define the middlewares' load order by putting their name in this array in the right order"
+ ],
+ "after": [
+ "parser",
+ "router"
+ ]
+ }
+}
+
timeout
: defines the maximum allowed milliseconds to load a middleware.load
:
+before
: array of middlewares that need to be loaded in the first place. The order of this array matters.order
: array of middlewares that need to be loaded in a specific order.after
: array of middlewares that need to be loaded at the end of the stack. The order of this array matters.Load a middleware at the very first place
Path — ./config/middleware.json
{
+ "timeout": 100,
+ "load": {
+ "before": [
+ "responseTime",
+ "logger"
+ ],
+ "order": [],
+ "after": []
+ }
+ }
+
The responseTime
middleware will be loaded first. Immediately followed by the logger
middleware. Then, the others middlewares will be loaded asynchronously.
Load a middleware after another one
Path — ./config/middleware.json
.
{
+ "timeout": 100,
+ "load": {
+ "before": [],
+ "order": [
+ "p3p",
+ "gzip"
+ ],
+ "after": []
+ }
+ }
+
The gzip
middleware will be loaded after the p3p
middleware. All the others will be loaded asynchronously.
Load a middleware at the very end
Path — ./config/middleware.json
.
{
+ "timeout": 100,
+ "load": {
+ "before": [
+ ...
+ ],
+ "order": [],
+ "after": [
+ "parser",
+ "router"
+ ]
+ }
+ }
+
The router
middleware will be loaded at the very end. The parser
middleware will be loaded after all the others and just before the router
middleware.
Complete example
For this example, we are going to imagine that we have 10 middlewares to load:
We assume that we set the ./config/middleware.json
file like this:
{
+ "timeout": 100,
+ "load": {
+ "before": [
+ "responseTime",
+ "logger",
+ "cors",
+ ],
+ "order": [
+ "p3p",
+ "gzip"
+ ],
+ "after": [
+ "parser",
+ "router"
+ ]
+ }
+ }
+
Here is the loader order:
+ ← + Logging + + Usage tracking + → +
In order to improve the product and understand how the community is using it, we are collecting non-sensitive data.
Here is the list of the collected data and why we need them.
We are not collecting sensitive data such as databases configurations, environment or custom variables. The data are encrypted and anonymised.
GDPR
The collected data are non-sensitive or personal data. We are compliant with the European recommendations (see our Privacy Policy).
You can disable the tracking by removing the uuid
property in the package.json
file at the root of your project.
+ ← + Middlewares + + Migration guides + → +
This object contains the controllers, models, services and configurations contained in the ./admin
folder.
Returns the Koa instance.
Returns a Promise
. When resolved, it means that the ./config/functions/bootstrap.js
has been executed. Otherwise, it throws an error.
You can also access to the bootstrap function through strapi.config.functions.boostrap
.
Returns an object that represents the configurations of the project. Every JavaScript or JSON file located in the ./config
folder will be parsed into the strapi.config
object.
Returns an object of the controllers wich is available in the project. Every JavaScript file located in the ./api/**/controllers
folder will be parsed into the strapi.controllers
object. Thanks to this object, you can access to every controller's actions everywhere in the project.
This object doesn't include the admin's controllers and plugin's controllers.
Returns an object of the hooks available in the project. Every folder that follows this pattern strapi-*
and located in the ./node_modules
or /hooks
folder will be mounted into the strapi.hook
object.
Returns an object of the Koa middlewares found in the ./node_modules
folder of the project. This reference is very useful for the Strapi's core.
Returns a function that parses the configurations, hooks, middlewares and APIs of your app. It also loads the middlewares and hooks with the previously loaded configurations. This method could be useful to update references available through the strapi
global variable without having to restart the server. However, without restarting the server, the new configurations will not be taken in account.
Returns the Logger (Pino) instance.
Returns an object of the middlewares available in the project. Every folder in the ./middlewares
folder will be also mounted into the strapi.middleware
object.
Returns an object of models available in the project. Every JavaScript or JSON file located in the ./api/**/models
folders will be parsed into the strapi.models
object. Also every strapi.models.**
object is merged with the model's instance returned by the ORM (Mongoose, Bookshelf). It allows to call the ORM methods through the strapi.models.**
object (ex: strapi.models.users.find()
).
Returns an object of plugins available in the project. Each plugin object contains the associated controllers, models, services and configurations contained in the ./plugins/**/
folder.
Returns a function that will returns the available queries for this model. This feature is only available inside the plugin's files (controllers, services, custom functions). For more details, see the [ORM queries section](../plugin-development/backend-development.md#ORM queries).
Returns a function that reloads the entire app (with downtime).
Returns the Router (Joi router) instance.
Returns the http.Server
instance.
Returns an object of services available in the project. Every JavaScript file located in the ./api/**/services
folders will be parsed into the strapi.services
object.
Returns a function that loads the configurations, middlewares and hooks. Then, it executes the bootstrap file, freezes the global variable and listens the configured port.
Returns a function that shuts down the server and destroys the current connections.
Returns a set of utils.
Strapi comes with a full featured Command Line Interface (CLI) which lets you scaffold and manage your project in seconds.
Create a new project
strapi new <name>
+
+options: [--dev|--dbclient=<dbclient> --dbhost=<dbhost> --dbport=<dbport> --dbname=<dbname> --dbusername=<dbusername> --dbpassword=<dbpassword> --dbssl=<dbssl> --dbauth=<dbauth>]
+
strapi new <name>
+Generates a new project called <name> and installs the default plugins through the npm registry.
strapi new <name> --dev
+Generates a new project called <name> and creates symlinks for the ./admin
folder and each plugin inside the ./plugin
folder. It means that the Strapi's development workflow has been set up on the machine earlier.
strapi new <name> --dbclient=<dbclient> --dbhost=<dbhost> --dbport=<dbport> --dbname=<dbname> --dbusername=<dbusername> --dbpassword=<dbpassword> --dbssl=<dbssl> --dbauth=<dbauth>
+Generates a new project called <name> and skip the interactive database configuration and initilize with these options. <dbclient> can be mongo
, postgres
, mysql
, sqlite3
or redis
. <dbssl> and <dbauth> are optional.
See the CONTRIBUTING guide for more details.
Scaffold a complete API with its configurations, controller, model and service.
strapi generate:api <name> [<attribute:type>]
+
+options: [--tpl <name>|--plugin <name>]
+
strapi generate:api <name>
+Generates an API called <name> in the ./api
folder at the root of your project.
strapi generate:api <name> <attribute:type>
+Generates an API called <name> in the ./api
folder at the root of your project. The model will already contain an attribute called <attribute> with the type property set to <type>.
Example: strapi generate:api product name:string description:text price:integer
strapi generate:api <name> --plugin <plugin>
+Generates an API called <name> in the ./plugins/<plugin>
folder.
Example: strapi generate:api product --plugin content-manager
strapi generate:api <name> --tpl <template>
+Generates an API called <name> in the ./api
folder which works with the given <template>. By default, the generated APIs are based on Mongoose.
Example: strapi generate:api product --tpl bookshelf
The first letter of the filename will be uppercased.
Create a new controller
strapi generate:controller <name>
+
+options: [--api <name>|--plugin <name>]
+
strapi generate:controller <name>
+Generates an empty controller called <name> in the ./api/<name>/controllers
folder.
Example: strapi generate:controller category
will create the controller at ./api/category/controllers/Category.js
.
strapi generate:controller <name> --api <api>
+Generates an empty controller called <name> in the ./api/<api>/controllers
folder.
Example: strapi generate:controller category --api product
will create the controller at ./api/product/controllers/Category.js
.
strapi generate:controller <name> --plugin <plugin>
+Generates an empty controller called <name> in the ./plugins/<plugin>/controllers
folder.
The first letter of the filename will be uppercased.
Create a new model
strapi generate:model <name> [<attribute:type>]
+
+options: [--api <name>|--plugin <name>]
+
strapi generate:model <name>
+Generates an empty model called <name> in the ./api/<name>/models
folder. It will create two files.
+The first one will be <name>.js which contains your lifecycle callbacks and another <name>.settings.json that will list your attributes and options.
Example: strapi generate:model category
will create these two files ./api/category/models/Category.js
and ./api/category/models/Category.settings.json
.
strapi generate:model <name> <attribute:type>
+Generates an empty model called <name> in the ./api/<name>/models
folder. The file <name>.settings.json will already contain a list of attribute with their associated <type>.
Example: strapi generate:model category name:string description:text
will create these two files ./api/category/models/Category.js
and ./api/category/models/Category.settings.json
. This last file will contain two attributes name
with the type string
and description
with type text
.
strapi generate:model <name> --api <api>
+Generates an empty model called <name> in the ./api/<api>/models
folder.
Example: strapi generate:model category --api product
will create these two files:
./api/product/models/Category.js
./api/product/models/Category.settings.json
.strapi generate:model <name> --plugin <plugin>
+Generates an empty model called <name> in the ./plugins/<plugin>/models
folder.
The first letter of the filename will be uppercased.
Create a new service
strapi generate:service <name>
+
+options: [--api <name>|--plugin <name>]
+
strapi generate:service <name>
+Generates an empty service called <name> in the ./api/<name>/services
folder.
Example: strapi generate:service category
will create the service at ./api/category/services/Category.js
.
strapi generate:service <name> --api <api>
+Generates an empty service called <name> in the ./api/<api>/services
folder.
Example: strapi generate:service category --api product
will create the service at ./api/product/services/Category.js
.
strapi generate:service <name> --plugin <plugin>
+Generates an empty service called <name> in the ./plugins/<plugin>/services
folder.
The first letter of the filename will be uppercased.
Create a new policy
strapi generate:policy <name>
+
+options: [--api <name>|--plugin <name>]
+
strapi generate:policy <name>
+Generates an empty policy called <name> in the ./config/policies
folder.
Example: strapi generate:policy isAuthenticated
will create the policy at ./config/policies/isAuthenticated.js
.
strapi generate:policy <name> --api <api>
+Generates an empty policy called <name> in the ./api/<api>/config/policies
folder. This policy will be scoped and only accessible by the <api> routes.
Example: strapi generate:policy isAuthenticated --api product
will create the policy at ./api/product/config/policies/isAuthenticated.js
.
strapi generate:policy <name> --plugin <plugin>
+Generates an empty policy called <name> in the ./plugins/<plugin>/config/policies
folder. This policy will be scoped and accessible only by the <plugin> routes.
Create a new plugin skeleton.
strapi generate:plugin <name>
+
strapi generate:plugin <name>
+Generates an empty plugin called <name> in the ./plugins
folder.
Example: strapi generate:plugin user
will create the plugin at ./plugins/user
.
Please refer to the plugin develoment documentation to know more.
Install a plugin in the project.
strapi install <name>
+
+options: [--dev]
+
strapi install <name>
+Installs a plugin called <name> in the ./plugins
folder.
Example: strapi install content-type-builder
will install the plugin at ./plugins/content-type-builder
.
strapi install <name> --dev
+It will create a symlink from the local Strapi repository plugin folder called <name> in the ./plugins
folder.
Example: strapi install content-type-builder --dev
will create a symlink from /path/to/the/repository/packages/strapi-plugin-content-type-builder
to ./plugins/content-type-builder
.
Checkout the CONTRIBUTING guide for more details about the local Strapi development workflow.
WARNING
You have to restart the server to load the plugin into your project.
Please refer to the plugins documentation to know more.
Uninstall a plugin from the project.
strapi uninstall <name>
+
strapi uninstall <name>
+Uninstalls a plugin called <name> in the ./plugins
folder.
Example: strapi uninstall content-type-builder
will remove the folder at ./plugins/content-type-builder
.
Please refer to the plugins documentation to know more.
Print the current globally installed Strapi version.
strapi version
+
List CLI commands.
strapi help
+
+ ← + API Reference + + Concepts + → +
By default, your project's structure will look like this:
/admin
: contains the vast majority of the admin's front-end and back-end logic./api
: contains the business logic of your project will be in this folder split in sub-folder per API.
+**
/config
: contains the API's configurations (routes
, policies
, etc)./controllers
: contains the API's controllers./models
: contains the API's models./services
: contains the API's services./node_modules
: contains the npm's packages used by the project./config
/environments
: contains the project's configurations per environment.
+/**
/development
custom.json
: contains the custom configurations for this environment.database.json
: contains the database connections for this environment.request.json
: contains the request settings for this environment.response.json
: contains the response settings for this environment.server.json
: contains the server settings for this environment./production
/staging
/functions
: contains lifecycle or generic functions of the project./locales
: contains the translation files used by the built-in i18n feature.application.json
: contains the general configurations of the project.custom.json
: contains the custom configurations of the project.hook.json
: contains the hook settings of the project.language.json
: contains the language settings of the project.middleware.json
: contains the middleware settings of the project./hooks
: contains the custom hooks of the project./middlewares
: contains the custom middlewares of the project./plugins
: contains the installed plugins in the project./public
: contains the file accessible to the outside world.Inside the /config
folder, every folder will be parsed and injected into the global object strapi.config
. Let's say, you added a folder named credentials
with two files stripe.json
and paypal.json
into it. The content of these files will be accessible through strapi.config.credentials.stripe
and strapi.config.credentials.paypal
.
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.
module.exports = {
+ // GET /hello
+ index: async (ctx) => {
+ ctx.send('Hello World!');
+ }
+};
+
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/
folders. 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. By convention, controllers' names should be Pascal-cased, so that every word in the file (include the first one) is capitalized User.js
, LegalEntity.js
.
Please refer to the controllers' guide for more informations.
Filters are a handy way to request data according to generic parameters. It makes filtering, sorting and paginating easy and reusable (eg. GET /user?_limit=30&name=John
).
Please refer to the filters' guide for more informations.
Models are a representation of the database's structure and lifecyle. They are split into two separate files. A JavaScript file that contains the lifecycle callbacks, and a JSON one that represents the data stored in the database and their format. The models also allow you to define the relationships between them.
Path — ./api/user/models/User.js
.
module.exports = {
+ // Before saving a value.
+ // Fired before an `insert` or `update` query.
+ beforeSave: (next) => {
+ // Use `this` to get your current object
+ next();
+ },
+
+ // After saving a value.
+ // Fired after an `insert` or `update` query.
+ afterSave: (doc, next) => {
+ next();
+ },
+
+ // ... and more
+};
+
Path — ./api/user/models/User.settings.json
.
{
+ "connection": "default",
+ "info": {
+ "name": "user",
+ "description": "This represents the User Model"
+ },
+ "attributes": {
+ "firstname": {
+ "type": "string"
+ },
+ "lastname": {
+ "type": "string"
+ }
+ }
+}
+
In this example, there is a User
model which contains two attributes firstname
and lastname
.
The 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 every where in the project, they contain the ORM model object that they are refer to. By convention, models' names should be written in lowercase.
A model must contain a list of attributes, and each of these attributes must have a type.
Please refer to the models' guide for more informations about the attributes.
Many-to-many associations allow to link an entry to many entry.
Please refer to the many-to-many guide
One-way relationships are useful to link an entry to another.
Please refer to the one-to-many guide
One-way relationships are useful to link an entry to another.
Please refer to the one-to-one guide.
One-way relationships are useful to link an entry to another. However, only one of the models can be queried with its populated items.
Please refer to the one-way guide.
Lifecycle callbacks are functions triggered at specific moments of the queries.
Please refer to the lifecycle callbacks guide.
Internationalization and localization (i18n) allows to adapt the project to different languages and serve the right content to the users. This feature is deeply integrated into the Strapi's core. It will detect the user language preference (locale) and translate the requested content using the translation files.
Please refer to the internationalization's guide.
A plugin is like a fully independent sub-application. It has its own business logic with dedicated models, controllers, services, middlewares or hooks. It can also contain an UI integrated into the admin panel to use it easily. It allows to develop or plugin features in a project in a short time span.
Please refer to the plugins documentation for more informations.
The admin panel uses Bootstrap to be styled on top of solid conventions and reusable CSS classes. It is also using PostCSS and PostCSS SCSS to keep the code maintainable.
Please refer to the plugin front-end development for detailed informations.
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.
Policies can be:
global
: so they can be used within the entire project.scoped
: used by single API or plugin.The API and plugins policies (scoped) 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
.
Please refer to the policy guide
Global policies are reusable through the entire app.
Please refer to the global policy guide
A policy defined in an API or plugin is usable only from this API or plugin. You don't need any prefix to use it.
Please refer to the scoped policy guide.
Plugin policies are usable from any app API.
Please refer to the plugin policy guide.
Public assets are static files such as images, video, css, etc that you want to make accessible to the outside world. Every new project includes by default, a folder named ./public
.
Please refer to the public configuration for more informations.
The context object (ctx
) contains all the request's related informations.
Please refer to the requests guide for more informations.
The context object (ctx
) contains a list of values and functions useful to manage server responses.
Please refer to the responses guide for more informations.
./api/**/config/routes.json
files define all available routes for the clients.
Please refer to the routing guide for more informations.
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.
Please refer to the services guide for more informations.
The main configurations of the project are located in the ./config
directory. Additional configs can be added in the ./api/**/config
folder of each API and plugin by creating JavaScript or JSON files.
Contains the main configurations relative to your project.
Path — ./config/application.json
.
{
+ "favicon": {
+ "path": "favicon.ico",
+ "maxAge": 86400000
+ },
+ "public": {
+ "path": "./public",
+ "maxAge": 60000
+ }
+}
+
favicon
path
(string): Path to the favicon file. Default value: favicon.ico
.maxAge
(integer): Cache-control max-age directive in ms. Default value: 86400000
.public
path
(string): Path to the public folder. Default value: ./public
.maxAge
(integer): Cache-control max-age directive in ms. Default value: 60000
.Add custom configurations to the project. The content of this file is available through the strapi.config
object.
Path — ./config/custom.json
.
{
+ "backendURL": "http://www.strapi.io",
+ "mainColor": "blue"
+}
+
These configurations are accessible through strapi.config.backendURL
and strapi.config.mainColor
.
As described in the i18n documentation, Strapi includes an internationalization system. This is especially useful to translate API messages (errors, etc.).
Path — ./config/language.json
.
{
+ "enabled": true,
+ "defaultLocale": "en_us",
+ "modes": [
+ "query",
+ "subdomain",
+ "cookie",
+ "header",
+ "url",
+ "tld"
+ ],
+ "cookieName": "locale"
+}
+
enabled
(boolean): Enable or disable i18n. Default value: true
.defaultLocale
(string): Default locale used by the application. Default value: en_us
.modes
(array): Methods used to detect client language. Default value: ["query", "subdomain", "cookie", "header", "url", "tld"]
.cookieName
(string): Name of the cookie used to store the locale name. Default value: locale
.The ./config/functions/
folder contains a set of JavaScript files in order to add dynamic and logic based configurations.
Path — ./config/functions/bootstrap.js
.
The bootstrap
function is called at every server start. You can use it to add a specific logic at this moment of your server's lifecycle.
Here are some use cases:
CRON tasks allow you to schedule jobs (arbitrary functions) for execution at specific dates, with optional recurrence rules. It only uses a single timer at any given time (rather than reevaluating upcoming jobs every second/minute).
Make sure the enabled
cron config is set to true
in your environment's variables.
The cron format consists of:
* * * * * *
+┬ ┬ ┬ ┬ ┬ ┬
+│ │ │ │ │ |
+│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun)
+│ │ │ │ └───── month (1 - 12)
+│ │ │ └────────── day of month (1 - 31)
+│ │ └─────────────── hour (0 - 23)
+│ └──────────────────── minute (0 - 59)
+└───────────────────────── second (0 - 59, OPTIONAL)
+
To define a CRON job, add your logic like bellow:
Path — ./config/functions/cron.js
.
module.exports = {
+
+ /**
+ * Simple example.
+ * Every monday at 1am.
+ */
+
+ '0 0 1 * * 1': () => {
+ // Add your own logic here (eg. send a queue of email, create a database backup, etc.).
+ }
+};
+
The locales
directory contains the translations of your API.
Each JSON file located in the folder must have the name of its corresponding translation (eg. en_US.json
, fr_FR.json
, etc.). Each line defines a translation key and its corresponding value.
Path — ./config/locales/en_US.json
.
{
+ "welcome": "Welcome"
+}
+
Take a look at the internationalization's guide for more details.
Most of the application's configurations are defined by environment. It means that you can specify settings for each environment (development
, production
, test
, etc.).
You can access the config of the current environment through strapi.config.currentEnvironment
.
Path — ./config/environments/**/database.json
.
defaultConnection
(string): Connection by default for models which are not related to a specific connection
. Default value: default
.connections
List of all available connections.
+default
connector
(string): Connector used by the current connection. Default value: strapi-hook-mongoose
.client
(string): Client used to store session. Default value: cookie
.key
(string): Cookie key name. Default value: strapi.sid
maxAge
(integer): Time in milliseconds before the session expire. Default value: 86400000
.rolling
(boolean): Force a session identifier cookie to be set on every response. Default value: false
.signed
(boolean): httpOnly or not. Default value: true
.overwrite
(boolean): Can overwrite or not. Default value: true
.settings
Useful for external session stores such as Redis.
+host
(string): Database host name. Default value: localhost
.port
(integer): Database port. Default value: 27017
.database
(string): Database name. Default value: development
.username
(string): Username used to establish the connection.password
(string): Password used to establish the connection.options
(object): List of additional options used by the connector.timezone
(string): Set the default behavior for local time (used only for a SQL database). Default value: utc
.options
Options used for database connection.
+ssl
(boolean): For ssl database connection.debug
(boolean): Show database exchanges and errors.autoMigration
(boolean): To disable auto tables/columns creation for SQL database.Path — ./config/environments/**/database.json
.
{
+ "defaultConnection": "default",
+ "connections": {
+ "default": {
+ "connector": "strapi-hook-mongoose",
+ "settings": {
+ "client": "mongo",
+ "host": "localhost",
+ "port": 27017,
+ "database": "development",
+ "username": "fooUsername",
+ "password": "fooPwd"
+ },
+ "options": {
+ "authenticationDatabase": "",
+ "ssl": true,
+ "minimize": true
+ }
+ },
+ "postgres": {
+ "connector": "strapi-hook-bookshelf",
+ "settings": {
+ "client": "postgres",
+ "host": "localhost",
+ "port": 5432,
+ "username": "${process.env.USERNAME}",
+ "password": "${process.env.PWD}",
+ "database": "strapi",
+ "schema": "public"
+ },
+ "options": {
+ "debug": true
+ }
+ },
+ "mysql": {
+ "connector": "strapi-hook-bookshelf",
+ "settings": {
+ "client": "mysql",
+ "host": "localhost",
+ "port": 5432,
+ "username": "strapi",
+ "password": "root",
+ "database": ""
+ },
+ "options": {}
+ },
+ "redis": {
+ "connector": "strapi-redis",
+ "settings": {
+ "port": 6379,
+ "host": "localhost",
+ "password": ""
+ },
+ "options": {
+ "debug": false
+ }
+ }
+ }
+}
+
Please refer to the dynamic configurations section to use global environment variable to configure the databases.
Path — ./config/environments/**/request.json
.
session
enabled
(boolean): Enable or disable sessions. Default value: false
.client
(string): Client used to persist sessions. Default value: redis
.settings
host
(string): Client host name. Default value: localhost
.port
(integer): Client port. Default value: 6379
.database
(integer)|String - Client database name. Default value: 10
.password
(string): Client password. Default value:
.logger
level
(string): Default log level. Default value: debug
.exposeInContext
(boolean): Expose logger in context so it can be used through strapi.log.info(‘my log’)
. Default value: true
.requests
(boolean): Enable or disable requests logs. Default value: false
.parser
enabled
(boolean): Enable or disable parser. Default value: true
.multipart
(boolean): Enable or disable multipart bodies parsing. Default value: true
.router
prefix
(string): API url prefix (eg. /v1
).The session doesn't work with mongo
as a client. The package that we should use is broken for now.
Path — ./config/environments/**/response.json
.
gzip
enabled
(boolean): Enable or not GZIP response compression.responseTime
enabled
(boolean): Enable or not X-Response-Time header
to response. Default value: false
.Path — ./config/environments/**/security.json
.
csrf
enabled
(boolean): Enable or disable CSRF. Default value: depends on the environment.key
(string): The name of the CSRF token added to the model. Default value: _csrf
.secret
(string): The key to place on the session object which maps to the server side token. Default value: _csrfSecret
.csp
enabled
(boolean): Enable or disable CSP to avoid Cross Site Scripting (XSS) and data injection attacks.p3p
enabled
(boolean): Enable or disable p3p.hsts
enabled
(boolean): Enable or disable HSTS.maxAge
(integer): Number of seconds HSTS is in effect. Default value: 31536000
.includeSubDomains
(boolean): Applies HSTS to all subdomains of the host. Default value: true
.xframe
enabled
(boolean): Enable or disable X-FRAME-OPTIONS
headers in response.value
(string): The value for the header, e.g. DENY, SAMEORIGIN or ALLOW-FROM uri. Default value: SAMEORIGIN
.xss
enabled
(boolean): Enable or disable XSS to prevent Cross Site Scripting (XSS) attacks in older IE browsers (IE8).cors
enabled
(boolean): Enable or disable CORS to prevent your server to be requested from another domain.origin
(string): Allowed URLs (http://example1.com, http://example2.com
or allows everyone *
). Default value: http://localhost
.expose
(array): Configures the Access-Control-Expose-Headers
CORS header. If not specified, no custom headers are exposed. Default value: ["WWW-Authenticate", "Server-Authorization"]
.maxAge
(integer): Configures the Access-Control-Max-Age
CORS header. Default value: 31536000
.credentials
(boolean): Configures the Access-Control-Allow-Credentials
CORS header. Default value: true
.methods
(array)|String - Configures the Access-Control-Allow-Methods
CORS header. Default value: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
.headers
(array): Configures the Access-Control-Allow-Headers
CORS header. If not specified, defaults to reflecting the headers specified in the request's Access-Control-Request-Headers header. Default value: ["Content-Type", "Authorization", "X-Frame-Options"]
.ip
enabled
(boolean): Enable or disable IP blocker. Default value: false
.whiteList
(array): Whitelisted IPs. Default value: []
.blackList
(array): Blacklisted IPs. Default value: []
.Path — ./config/environments/**/server.json
.
host
(string): Host name. Default value: localhost
.port
(integer): Port on which the server should be running. Default value: 1337
.autoReload
(boolean): Enable or disabled server reload on files update. Default value: depends on the environment.cron
enabled
(boolean): Enable or disable CRON tasks to schedule jobs at specific dates. Default value: false
.admin
path
(string): Allow to change the URL to access the admin (default: /admin
).build
host
(string): URL to access the admin panel (default: http://localhost:1337/admin
).backend
(string): URL that the admin panel and plugins will request (default: http://localhost:1337
).
+plugins
source
(string): Define the source mode (origin, host, custom).folder
(string): Indicate what the plugins folder in host
source mode.For security reasons, sometimes it's better to set variables through the server environment. It's also useful to push dynamics values into configurations files. To enable this feature into JSON files, Strapi embraces a JSON-file interpreter into his core to allow dynamic value in the JSON configurations files.
The syntax is inspired by the template literals ES2015 specifications. These dynamic values are indicated by the Dollar sign and curly braces (${expression}
).
In any JSON configurations files in your project, you can inject dynamic values like this:
Path — ./config/environments/production/database.json
.
{
+ "defaultConnection": "default",
+ "connections": {
+ "default": {
+ "connector": "strapi-hook-mongoose",
+ "settings": {
+ "client": "mongo",
+ "uri": "${process.env.DATABASE_URI || ''}",
+ "host": "${process.env.DATABASE_HOST || '127.0.0.1'}",
+ "port": "${process.env.DATABASE_PORT || 27017}",
+ "database": "${process.env.DATABASE_NAME || 'production'}",
+ "username": "${process.env.DATABASE_USERNAME || ''}",
+ "password": "${process.env.DATABASE_PASSWORD || ''}"
+ },
+ "options": {}
+ }
+ }
+}
+
You can't execute functions inside the curly braces. Only strings are allowed.
Configuration files are not multi server friendly. So we create a data store for config you will want to update in production.
environment
(string): Sets the environment you want to store the data in. By default it's current environment (can be an empty string if your config is environment agnostic).type
(string): Sets if your config is for an api
, plugin
or core
. By default it's core
.name
(string): You have to set the plugin or api name if type
is api
or plugin
.key
(string, required): The name of the key you want to store.// strapi.store(object).get(object);
+
+// create reusable plugin store variable
+const pluginStore = strapi.store({
+ environment: strapi.config.environment,
+ type: 'plugin',
+ name: 'users-permissions'
+});
+
+await pluginStore.get({key: 'grant'});
+
value
(any, required): The value you want to store.// strapi.store(object).set(object);
+
+// create reusable plugin store variable
+const pluginStore = strapi.store({
+ environment: strapi.config.environment,
+ type: 'plugin',
+ name: 'users-permissions'
+});
+
+await pluginStore.set({
+ key: 'grant',
+ value: {
+ ...
+ }
+});
+
+ ← + Concepts + + Authentication + → +
Installation is very easy and only takes a few seconds.
Please make sure your computer/server meets the following requirements:
Time to install Strapi!
npm install strapi@alpha -g
+
If you encounter npm permissions issues, change the permissions to npm default directory.
It takes about 20 seconds with a good Internet connection. You can take a coffee ☕️ if you have a slow one.
Having troubles during the installation? Check if someone already had the same issue https://github.com/strapi/strapi/issues. If not, you can post one, or ask for help https://strapi.io/support.
Once completed, please check that the installation went well, by running:
strapi -v
+
That should print 3.0.0-alpha.x
.
Strapi is installed globally on your computer. Type strapi
in your terminal you will have access to every available command lines.
Congrats! Now that Strapi is installed you can create your first API.
This section explains how to handle Strapi for the first time, (check out our tutorial video).
Table of contents:
Creating your first project with Strapi is easy:
#1 — Open your terminal
Open your terminal in the directory you want to create your application in.
#2 — Run the following command line in your terminal:
strapi new my-project
+
This action creates a new folder named my-project
with the entire files structure of a Strapi application.
#3 — Go to your project and launch the server:
In your terminal run the following commands:
cd my-project
+strapi start
+
Now that your app is running let's see how to create your first user.
In order to use the admin panel and to consume your API you first need to register your first user. This process only happens once if you don't have any user table created and is made to create the admin user
that has all the permissions granted for your API.
If your using MongoDB for your database you don't have to create your table manually (it's already handled by Strapi) otherwise you'll have to create your user table first.
To create your first user, start your server (strapi start
) and go to : http://localhost:1337/admin.
Now that your first user is registered let's see how to create your first api.
To create your first API, start your server (strapi start
) and go to : http://localhost:1337/admin.
At this point, your application is empty. To create your first API is to use the Content Type Builder plugin: a powerful UI to help you create an API in a few clicks. Let's take the example of an e-commerce API, which manages products.
#1 — Go to the Content Type Builder plugin.
#2 — Create a Content Type named Product
and submit the form.
#3 — Add three fields in this Content Type.
string
field named name
.text
field named description
.number
field named price
(with float
as number format).#4 — Save. That's it!
See the CLI documentation for more information on how to do it the hacker way.
A new directory has been created in the ./api
folder of your application which contains all the needed stuff for your Product
Content Type: routes, controllers, services and models. Take a look at the API structure documentation for more informations.
Well done, you created your first API using Strapi!
After creating your first Content Type, it would be great to be able to create, edit or delete entries.
#1 — Go to the Product list by clicking on the link in the left menu (generated by the Content Manager plugin).
#2 — Click on the button Add New Product
and fill the form.
#3 — Save! You can edit or delete this entry by clicking on the icons at the right of the row.
Your API is now ready and contains data. At this point, you'll probably want to use this data in mobile or desktop applications. +In order to do so, you'll need to allow access to other users (identified as 'Guest').
1 - Go to the Auth & Permissions View by clicking on Auth & Permissions link in the left menu and click on the Guest Role item.
2 - Manage your APIs permissions in the Permissions section of the Edit Guest Role view by enabling or disabling specific actions.
To retrieve the list of products, use the GET /your-content-type
route.
Generated APIs provide a handy way to filter and order queries. In that way, ordering products by price is as easy as GET http://localhost:1337/product?_sort=price:asc
. For more informations, read the filters documentation
Here is an example using jQuery.
$.ajax({
+ type: 'GET',
+ url: 'http://localhost:1337/product?_sort=price:asc', // Order by price.
+ done: function(products) {
+ console.log('Well done, here is the list of products: ', products);
+ },
+ fail: function(error) {
+ console.log('An error occurred:', error);
+ }
+});
+
If you want to get a specific entry, add the id
of the wanted product at the end of the url.
$.ajax({
+ type: 'GET',
+ url: 'http://localhost:1337/product/123', // Where `123` is the `id` of the product.
+ done: function(product) {
+ console.log('Well done, here is the product having the `id` 123: ', product);
+ },
+ fail: function(error) {
+ console.log('An error occurred:', error);
+ }
+});
+
Use the POST
route to create a new entry.
jQuery example:
$.ajax({
+ type: 'POST',
+ url: 'http://localhost:1337/product',
+ data: {
+ name: 'Cheese cake',
+ description: 'Chocolate cheese cake with ice cream',
+ price: 5
+ },
+ done: function(product) {
+ console.log('Congrats, your product has been successfully created: ', product); // Remember the product `id` for the next steps.
+ },
+ fail: function(error) {
+ console.log('An error occurred:', error);
+ }
+});
+
Use the PUT
route to update an existing entry.
jQuery example:
$.ajax({
+ type: 'PUT',
+ url: 'http://localhost:1337/product/123', // Where `123` is the `id` of the product.
+ data: {
+ description: 'This is the new description'
+ },
+ done: function(product) {
+ console.log('Congrats, your product has been successfully updated: ', product.description);
+ },
+ fail: function(error) {
+ console.log('An error occurred:', error);
+ }
+});
+
Use the DELETE
route to delete an existing entry.
jQuery example:
$.ajax({
+ type: 'DELETE',
+ url: 'http://localhost:1337/product/123', // Where `123` is the `id` of the product.
+ done: function(product) {
+ console.log('Congrats, your product has been successfully deleted: ', product);
+ },
+ fail: function(error) {
+ console.log('An error occurred:', error);
+ }
+});
+
Congratulations! You successfully finished the Getting Started guide! Read the concepts to understand more advanced concepts.
Also, feel free to join the community thanks to the different channels listed in the community page: team members, contributors and developers will be happy to help you.
+ ← + Installation + + API Reference + → +
WARNING
This feature requires the Users & Permissions plugin (installed by default).
This route lets you create new users.
$.ajax({
+ type: 'POST',
+ url: 'http://localhost:1337/auth/local/register',
+ data: {
+ username: 'Strapi user',
+ email: 'user@strapi.io',
+ password: 'strapiPassword'
+ },
+ done: function(auth) {
+ console.log('Well done!');
+ console.log('User profile', auth.user);
+ console.log('User token', auth.jwt);
+ },
+ fail: function(error) {
+ console.log('An error occurred:', error);
+ }
+});
+
This route lets you login your users by getting an authentication token.
identifier
param can either be an email or a username.$.ajax({
+ type: 'POST',
+ url: 'http://localhost:1337/auth/local',
+ data: {
+ identifier: 'user@strapi.io',
+ password: 'strapiPassword'
+ },
+ done: function(auth) {
+ console.log('Well done!');
+ console.log('User profile', auth.user);
+ console.log('User token', auth.jwt);
+ },
+ fail: function(error) {
+ console.log('An error occurred:', error);
+ }
+});
+
Thanks to Grant and Purest, you can easily use OAuth and OAuth2 +providers to enable authentication in your application. By default, +Strapi comes with the following providers:
👀 See our complete example with detailed tutorials for each provider (with React)
To use the providers authentication, set your credentials in the admin interface (Plugin Users & Permissions > Providers). +Then update and enable the provider you want use.
Redirect your user to: GET /connect/:provider
. eg: GET /connect/facebook
After his approval, he will be redirected to /auth/:provider/callback
. The jwt
and user
data will be available in the body response.
Response payload:
{
+ "user": {},
+ "jwt": ""
+}
+
By default, each API request is identified as guest
role (see permissions of guest
's role in your admin dashboard). To make a request as a user, you have to set the Authorization
token in your request headers. You receive a 401 error if you are not authorized to make this request or if your authorization header is not correct.
token
variable is the data.jwt
received when login in or registering.$.ajax({
+ type: 'GET',
+ url: 'http://localhost:1337/article',
+ headers: {
+ Authorization: `Bearer ${token}`
+ },
+ done: function(data) {
+ console.log('Your data', data);
+ },
+ fail: function(error) {
+ console.log('An error occurred:', error);
+ }
+});
+
This action sends an email to a user with the link of you reset password page. This link contains an URL param code
which is required to reset user password.
email
is your user email.url
is the url link that user will receive.$.ajax({
+ type: 'POST',
+ url: 'http://localhost:1337/auth/forgot-password',
+ data: {
+ email: 'user@strapi.io',
+ url: 'http://mon-site.com/rest-password'
+ },
+ done: function() {
+ console.log('Your user received an email');
+ },
+ fail: function(error) {
+ console.log('An error occurred:', error);
+ }
+});
+
Received link url format http://mon-site.com/rest-password?code=privateCode
This action will reset the user password.
code
is the url params received from the email link (see forgot password)$.ajax({
+ type: 'POST',
+ url: 'http://localhost:1337/auth/reset-password',
+ data: {
+ code: 'privateCode',
+ password: 'myNewPassword',
+ passwordConfirmation: 'myNewPassword'
+ },
+ done: function() {
+ console.log('Your user password is reset');
+ },
+ fail: function(error) {
+ console.log('An error occurred:', error);
+ }
+});
+
The User object is available to successfully authenticated requests.
user
object is a property of ctx.state
. create: async (ctx) => {
+
+ const { _id } = ctx.state.user
+
+ const depositObj = {
+ ...ctx.request.body,
+ depositor: _id
+ }
+
+ const data = await strapi.services.deposit.add(depositObj);
+
+ // Send 201 `created`
+ ctx.created(data);
+ }
+
+
+ ← + Configurations + + Controllers + → +
See the controllers' concepts for details.
There are two ways to create a controller:
strapi generate:controller user
. Read the CLI documentation for more information.User.js
in ./api/**/controllers
which contains at least one endpoint.Each controller’s action must be an async
function and receives the context
(ctx
) object as first parameter containing the request context and the response context. The action has to be bounded by a route.
In this example, we are defining a specific route in ./api/hello/config/routes.json
that takes Hello.index
as handler. It means that every time a web browser is pointed to the /hello
URL, the server will called 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
.
{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/hello",
+ "handler": "Hello.index"
+ }
+ ]
+}
+
Path — ./api/hello/controllers/Hello.js
.
module.exports = {
+ // GET /hello
+ index: async (ctx) => {
+ ctx.send('Hello World!');
+ }
+};
+
A route handler can only access the controllers defined in the ./api/**/controllers
folders.
+ ← + Authentication + + Deployment + → +
Update the production
settings with the IP and domain name where the project will be running.
Path — ./config/environments/production/server.json
.
{
+ "host": "domain.io", // IP or domain
+ "port": 1337,
+ "autoReload": {
+ "enabled": false
+ },
+ "admin": {
+ "path": "/dashboard" // We highly recommend to change the default `/admin` path for security reasons.
+ }
+}
+
⚠️ If you changed the path to access to the administration, the step #2 is required.
Run this following command to install the dependencies and build the project with your custom configurations.
cd /path/to/the/project
+npm run setup
+
To display the build logs use the --debug option npm run setup --debug
.
Run the server with the production
settings.
NODE_ENV=production npm start
+
WARNING
We highly recommend to use pm2 to manage your process.
If you want to host the administration on another server than the API, please take a look at this dedicated section.
+ ← + Controllers + + Email + → +
WARNING
This feature requires the Email plugin (installed by default).
Thanks to the plugin Email
, you can send email on your server or externals providers such as Sendgrid.
await strapi.plugins['email'].services.email.send({
+ to: 'admin@strapi.io',
+ from: 'robbot@strapi.io',
+ replyTo: 'no-reply@strapi.io',
+ subject: 'Use strapi email provider successfully',
+ text: 'Hello world!',
+ html: 'Hello world!'
+});
+
By default Strapi provides a local email system. You might want to send email with Sendgrid or another provider.
To install a new provider run:
$ npm install strapi-email-sendgrid@alpha --save
+
We have two providers available strapi-email-sendgrid
and strapi-upload-mailgun
, use the alpha tag to install one of them. Then, visit /admin/plugins/email/configurations/development
on your web browser and configure the provider.
If you want to create your own, make sure the name starts with strapi-email-
(duplicating an existing one will be easier to create), modify the auth
config object and customize the send
functions.
Check all community providers available on npmjs.org - Providers list
+ ← + Deployment + + Filters + → +
See the filters' concepts for details.
by default, the filters can only be used from find
endpoints generated by the Content Type Builder and the CLI. If you need to implement a filters system somewhere else, read the programmatic usage section.
The available operators are separated in four different categories:
Easily filter results according to fields values.
=
: Equals_ne
: Not equals_lt
: Lower than_gt
: Greater than_lte
: Lower than or equal to_gte
: Greater than or equal to_contains
: Contains_containss
: Contains case sensitiveFind users having John
as first name.
GET /user?firstName=John
Find products having a price equal or greater than 3
.
GET /product?price_gte=3
Sort according to a specific field.
Sort users by email.
GET /user?_sort=email:asc
GET /user?_sort=email:desc
Limit the size of the returned results.
Limit the result length to 30.
GET /user?_limit=30
Skip a specific number of entries (especially useful for pagination).
Get the second page of results.
GET /user?_start=10&_limit=10
Requests system can be implemented in custom code sections.
To extract the filters from an JavaScript object or a request, you need to call the strapi.utils.models.convertParams
helper.
The returned objects is formatted according to the ORM used by the model.
Path — ./api/user/controllers/User.js
.
// Define a list of params.
+const params = {
+ '_limit': 20,
+ '_sort': 'email'
+};
+
+// Convert params.
+const formattedParams = strapi.utils.models.convertParams('user', params); // { limit: 20, sort: 'email' }
+
Path — ./api/user/controllers/User.js
.
module.exports = {
+
+ find: async (ctx) => {
+ // Convert params.
+ const formattedParams = strapi.utils.models.convertParams('user', ctx.request.query);
+
+ // Get the list of users according to the request query.
+ const filteredUsers = await User
+ .find()
+ .where(formattedParams.where)
+ .sort(formattedParams.sort)
+ .skip(formattedParams.start)
+ .limit(formattedParams.limit);
+
+ // Finally, send the results to the client.
+ ctx.body = filteredUsers;
+ };
+};
+
+ ← + Email + + GraphQL + → +
WARNING
This feature requires the GraphQL plugin (not installed by default).
To get started with GraphQL in your app, please install the plugin first. To do that, open your terminal and run the following command:
strapi install graphql
+
Then, start your app and open your browser at http://localhost:1337/playground. You should see the interface (GraphQL Playground) that will help you to write GraphQL query to explore your data.
Install the ModHeader extension to set the
Authorization
header in your request
By default, the Shadow CRUD feature is enabled and the GraphQL is set to /graphql
. You can edit these configurations in the following files.
Path — ./plugins/graphql/config/settings.json
.
{
+ "endpoint": "/graphql",
+ "shadowCRUD": true,
+ "depthLimit": 7
+}
+
In the section, we assume that the Shadow CRUD feature is enabled. For each model, the plugin auto-generates queries which just fit to your needs.
id
: Stringquery {
+ user(id: "5aafe871ad624b7380d7a224") {
+ username
+ email
+ }
+}
+
query {
+ users {
+ username
+ email
+ }
+}
+
Filters
You can also apply different parameters to the query to make more complex queries.
limit
(integer): Define the number of returned entries.start
(integer): Define the amount of entries to skip.sort
(string): Define how the data should be sorted.where
(object): Define the filters to apply in the query.
+<field>
: Equals.<field>_ne
: Not equals.<field>_lt
: Lower than.<field>_lte
: Lower than or equal to.<field>_gt
: Greater than.<field>_gte
: Lower than or equal to.<field>_contains
: Contains.<field>_containss
: Contains sensitive.Return the second decade of users which have an email that contains @strapi.io
ordered by username.
query {
+ users(limit: 10, start: 10, sort: "username:asc", where: {
+ email_contains: "@strapi.io"
+ }) {
+ username
+ email
+ }
+}
+
Return the users which have been created after the March, 19th 2018 4:21 pm.
query {
+ users(where: {
+ createdAt_gt: "2018-03-19 16:21:07.161Z"
+ }) {
+ username
+ email
+ }
+}
+
To simplify and automate the build of the GraphQL schema, we introduced the Shadow CRUD feature. It automatically generates the type definition, queries and resolvers based on your models. The feature also lets you make complex query with many arguments such as limit
, sort
, start
and where
.
If you've generated an API called Post
using the CLI strapi generate:api post
or the administration panel, your model looks like this:
Path — ./api/post/models/Post.settings.json
.
{
+ "connection": "default",
+ "options": {
+ "timestamps": true
+ },
+ "attributes": {
+ "title": {
+ "type": "string"
+ }
+ "content": {
+ "type": "text"
+ },
+ "published": {
+ "type": "boolean"
+ }
+ }
+}
+
The generated GraphQL type and queries will be:
type Post {
+ _id: String
+ created_at: String
+ updated_at: String
+ title: String
+ content: String
+ published: Boolean
+}
+
+type Query {
+ posts(sort: String, limit: Int, start: Int, where: JSON): [Post]
+ post(id: String!): Post
+}
+
The query will use the generated controller's actions as resolvers. It means that the posts
query will execute the Post.find
action and the post
query will use the Post.findOne
action.
This feature is only available on Mongoose ORM.
Strapi now supports Aggregation & Grouping. +Let's consider again the model mentioned above:
type Post {
+ _id: ID
+ createdAt: String
+ updatedAt: String
+ title: String
+ content: String
+ nb_likes: Int,
+ published: Boolean
+}
+
+
Strapi will generate automatically for you the following queries & types:
type PostConnection {
+ values: [Post]
+ groupBy: PostGroupBy
+ aggregate: PostAggregator
+}
+
+type PostGroupBy {
+ _id: [PostConnection_id]
+ createdAt: [PostConnectionCreatedAt]
+ updatedAt: [PostConnectionUpdatedAt]
+ title: [PostConnectionTitle]
+ content: [PostConnectionContent]
+ nb_likes: [PostConnectionNbLikes],
+ published: [PostConnectionPublished]
+}
+
+type PostConnectionPublished {
+ key: Boolean
+ connection: PostConnection
+}
+
+type PostAggregator {
+ count: Int
+ sum: PostAggregatorSum
+ avg: PostAggregatorAvg
+ min: PostAggregatorMin
+ max: PostAggregatorMax
+}
+
+type PostAggregatorAvg {
+ nb_likes: Float
+}
+
+type PostAggregatorMin { // Same for max and sum
+ nb_likes: Int
+}
+
+type Query {
+ postsConnection(sort: String, limit: Int, start: Int, where: JSON): PostConnection
+}
+
Getting the total count and the average likes of posts:
postsConnection {
+ aggregate {
+ count
+ avg {
+ nb_likes
+ }
+ }
+
+}
+
Let's say we want to do the same query but for only published posts
postsConnection(where: { published: true }) {
+ aggregate {
+ count
+ avg {
+ nb_likes
+ }
+ }
+
+}
+
Gettings the average likes of published and unpublished posts
postsConnection {
+ groupBy {
+ published: {
+ key
+ connection {
+ aggregate {
+ avg {
+ nb_likes
+ }
+ }
+ }
+ }
+ }
+}
+
Result
{
+ data: {
+ postsConnection: {
+ groupBy: {
+ published: [
+ {
+ key: true,
+ connection: {
+ aggregate: {
+ avg {
+ nb_likes: 10
+ }
+ }
+ }
+ },
+ {
+ key: false,
+ connection: {
+ aggregate: {
+ avg {
+ nb_likes: 0
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+}
+
If you want to define a new scalar, input or enum types, this section is for you. To do so, you will have to create a schema.graphql
file. This file has to be placed into the config folder of each API ./api/*/config/schema.graphql
or plugin ./plugins/*/config/schema.graphql
.
Structure — schema.graphql
.
module.exports = {
+ definition: ``,
+ query: ``,
+ type: {},
+ resolver: {
+ Query: {}
+ }
+};
+
definition
(string): let's you define new type, input, etc.query
(string): where you add custom query.type
(object): allows you to add description, deprecated field or disable the Shadow CRUD feature on a specific type.resolver
(object):
+Query
(object): let's you define custom resolver, policies for a query.Let say we are using the same previous Post
model.
Path — ./api/post/config/schema.graphql
.
module.exports = {
+ definition: `
+ enum PostStatusInput {
+ draft
+ reviewing
+ reviewed
+ published
+ deleted
+ }
+ `,
+ query: `
+ postsByAuthor(id: String, status: PostStatusInput, limit: Int): [Post]!
+ `,
+ resolver: {
+ Query: {
+ post: {
+ description: 'Return a single post',
+ policy: ['plugins.users-permissions.isAuthenticated', 'isOwner'], // Apply the 'isAuthenticated' policy of the `Users & Permissions` plugin, then the 'isOwner' policy before executing the resolver.
+ },
+ posts: {
+ description: 'Return a list of posts', // Add a description to the query.
+ deprecated: 'This query should not be used anymore. Please consider using postsByAuthor instead.'
+ },
+ postsByAuthor: {
+ description: 'Return the posts published by the author',
+ resolver: 'Post.findByAuthor'
+ },
+ postsByTags: {
+ description: 'Return the posts published by the author',
+ resolverOf: 'Post.findByTags', // Will apply the same policy on the custom resolver than the controller's action `findByTags`.
+ resolver: (obj, options, ctx) => {
+ // ctx is the context of the Koa request.
+ await strapi.controllers.posts.findByTags(ctx);
+
+ return ctx.body.posts || `There is no post.`;
+ }
+ }
+ }
+ }
+};
+
Edit the definition
attribute in one of the schema.graphql
files of your project by using the GraphQL Type language string.
The easiest way is to create a new model using the CLI strapi generate:model category --api post
, so you don't need to customise anything.
module.exports = {
+ definition: `
+ type Person {
+ id: Int!
+ firstname: String!
+ lastname: String!
+ age: Int
+ children: [Person]
+ }
+ `
+};
+
To explore the data of the new type Person
, you need to define a query and associate a resolver to this query.
module.exports = {
+ definition: `
+ type Person {
+ id: Int!
+ firstname: String!
+ lastname: String!
+ age: Int
+ children: [Person]
+ }
+ `,
+ query: `
+ person(id: Int!): Person
+ `,
+ type: {
+ Person: {
+ _description: 'The Person type description', // Set the description for the type itself.
+ firstname: 'The firstname of the person',
+ lastname: 'The lastname of the person',
+ age: {
+ description: 'The age of the person',
+ deprecated: 'We are not using the age anymore, we can find it thanks to our powerful AI'
+ },
+ children: 'The children of the person'
+ }
+ }
+ resolver: {
+ Query: {
+ person: {
+ description: 'Return a single person',
+ resolver: 'Person.findOne' // It will use the action `findOne` located in the `Person.js` controller*.
+ }
+ }
+ }
+};
+
The resolver parameter also accepts an object as a value to target a controller located in a plugin.
module.exports = {
+ ...
+ resolver: {
+ Query: {
+ person: {
+ description: 'Return a single person',
+ resolver: {
+ plugin: 'users-permissions',
+ handler: 'User.findOne' // It will use the action `findOne` located in the `Person.js` controller inside the plugin `Users & Permissions`.
+ }
+ }
+ }
+ }
+};
+
One of the most powerful features of GraphQL is the auto-documentation of the schema. The GraphQL plugin allows you to add a description to a type, a field and a query. You can also deprecate a field or a query.
Path — ./api/post/models/Post.settings.json
.
{
+ "connection": "default",
+ "info": {
+ "description": "The Post type description"
+ },
+ "options": {
+ "timestamps": true
+ },
+ "attributes": {
+ "title": {
+ "type": "string",
+ "description": "The title of the post",
+ "deprecated": "We are not using the title anymore, it is auto-generated thanks to our powerful AI"
+ },
+ "content": {
+ "type": "text",
+ "description": "The content of the post."
+ },
+ "published": {
+ "type": "boolean",
+ "description": "Is the post published or not. Yes = true."
+ }
+ }
+}
+
It might happens that you want to add a description to a query or deprecate it. To do that, you need to use the schema.graphql
file.
Remember: The
schema.graphql
file has to be placed into the config folder of each API./api/*/config/schema.graphql
or plugin./plugins/*/config/schema.graphql
.
Path — ./api/post/config/schema.graphql
.
module.exports = {
+ resolver: {
+ Query: {
+ posts: {
+ description: 'Return a list of posts', // Add a description to the query.
+ deprecated: 'This query should not be used anymore. Please consider using postsByAuthor instead.' // Deprecate the query and explain the reason why.
+ }
+ }
+ }
+};
+
Sometimes a query needs to be only accessible to authenticated user. To handle this, Strapi provides a solid policy system. A policy is a function executed before the final action (the resolver). You can define an array of policy that will be executed in order.
module.exports = {
+ resolver: {
+ Query: {
+ posts: {
+ description: 'Return a list of posts',
+ policy: ['plugins.users-permissions.isAuthenticated', 'isOwner', 'global.logging']
+ }
+ }
+ }
+};
+
In this example, the policy isAuthenticated
located in ./plugins/users-permissions/config/policies/isAuthenticated.js
will be executed first. Then, the isOwner
policy located in the Post
API ./api/post/config/policies/isOwner.js
. Next, it will execute the logging
policy located in ./config/policies/logging.js
. Finally, the resolver will be executed.
There is no custom resolver in that case, so it will execute the default resolver (Post.find) provided by the Shadow CRUD feature.
By default, the plugin will execute the actions located in the controllers that has been generated via the Content-Type Builder plugin or the CLI. For example, the query posts
is going to execute the logic inside the find
action in the Post.js
controller. It might happens that you want to execute another action or a custom logic for one of your query.
module.exports = {
+ resolver: {
+ Query: {
+ posts: {
+ description: 'Return a list of posts by author',
+ resolver: 'Post.findByAuthor'
+ }
+ }
+ }
+};
+
In this example, it will execute the findByAuthor
action of the Post
controller. It also means that the resolver will apply on the posts
query the permissions defined on the findByAuthor
action (through the administration panel).
The obj
parameter is available via ctx.params
and the options
are available via ctx.query
in the controller's action.
module.exports = {
+ resolver: {
+ Query: {
+ posts: {
+ description: 'Return a list of posts by author',
+ resolver: (obj, options, context) => {
+ // You can return a raw JSON object or a promise.
+
+ return [{
+ title: 'My first blog post',
+ content: 'Whatever you want...'
+ }];
+ }
+ }
+ }
+ }
+};
+
You can also execute a custom logic like above. However, the roles and permissions layers won't work.
It might happens that you want apply our permissions layer on a query. That's why, we created the resolverOf
attribute. This attribute defines which are the permissions that should be applied to this resolver. By targeting an action it means that you're able to edit permissions for this resolver directly from the administration panel.
module.exports = {
+ resolver: {
+ Query: {
+ posts: {
+ description: 'Return a list of posts by author',
+ resolverOf: 'Post.find', // Will apply the same policy on the custom resolver than the controller's action `find` located in `Post.js`.
+ resolver: (obj, options, context) => {
+ // You can return a raw JSON object or a promise.
+
+ return [{
+ title: 'My first blog post',
+ content: 'Whatever you want...'
+ }];
+ }
+ }
+ }
+ }
+};
+
To do that, we need to use the schema.graphql
like below:
module.exports = {
+ type: {
+ Post: false // The Post type won't be "queriable".
+ }
+ resolver: {
+ Query: {
+ posts: false // The `posts` query will no longer be in the GraphQL schema.
+ }
+ }
+};
+
How are the types name defined?
The type name is the global ID of the model. You can find the global ID of a model like that strapi.models[xxx].globalId
or strapi.plugins[xxx].models[yyy].globalId
.
Where should I put the field description and deprecated reason?
We recommend to put the field description and deprecated reason in the model. Right now, the GraphQL plugin is the only which uses these fields. Another plugin could use this description in the future as well. However, sometimes you don't have the choice, especially when you're defining a custom type.
It's not a bad practice to put the description and deprecated attribute in the schema.graphql
, though.
Why are the "createdAt" and "updatedAt" field added to my type?
The plugin detects if the timestamps
option is set to true
in the model. By default, when you generate an API this option is checked. Set it to false
in your model to remove these fields.
+ ← + Filters + + Internationalization + → +
See the internationalization' concepts for details.
Because an API may need to send different data based on the language of the user, Strapi provides a built-in strategy to handle the internationalization (i18n).
The i18n
method that will allow you to retrieve the right string based on the language is accessible through the request's context.
There are many strategies to define the language that the server should use to return the correct translation. It can be based on the locale
query parameter, the cookie
or the Accept-Language
header.
locale
parameter in the URL GET /hello/John?locale=en_US
.locale
field in the cookie locale=en\-US;
.Accept-Language
header with the value en_US
.Please refer to the language configuration
Let's say we want to say Hello John
in english and Bonjour Tom
in french. We need to use the built-in i18n
feature and replace the string based on the received name.
Path — ./api/hello/config/routes.json
.
{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/hello/:name",
+ "handler": "Hello.sayHello"
+ }
+ ]
+}
+
Path — ./api/hello/controllers/Hello.js
.
module.exports = {
+ // GET /hello/:name
+ sayHello: async (ctx) => {
+ ctx.send(ctx.i18n.__('Hello %s', ctx.params.name));
+ }
+};
+
You need to define the english and french translation for this key.
Path — ./config/locales/en_US.json
.
{
+ "Hello %s": "Hello %s"
+}
+
Path — ./config/locales/fr_FR.json
.
{
+ "Hello %s": "Bonjour %s"
+}
+
That's all! The request GET /hello/John?locale=en_US
will return Hello John
and GET /hello/Tom?locale=fr_FR
will return Bonjour Tom
.
+ ← + GraphQL + + Models + → +
See the models' concepts for details.
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 fast feeling for available features.
Use the CLI, and run the following command strapi generate:model user firstname:string lastname:string
. Read the CLI documentation for more informations.
This will create two files located at ./api/user/models
:
User.settings.json
: contains the list of attributes and settings. The JSON format makes the file easily editable.User.js
: imports User.settings.json
and extends it with additional settings and lifecycle callbacks.when you create a new API using the CLI (strapi generate:api <name>
), a model is automatically created.
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.mainField
: Determines which model-attribute is shown when displaying the model.The options key on the model-json states.
idAttribute
: This tells the model which attribute to expect as the unique identifier for each database row (typically an auto-incrementing primary key named 'id').idAttributeType
: Data type of idAttribute
, accepted list of value bellow:The following types are currently available:
string
text
integer
biginteger
float
decimal
password
date
time
datetime
timestamp
boolean
binary
uuid
enumeration
json
email
You can apply basic validations to the attributes. The following supported validations are only supported by MongoDB connection. +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.max
(integer) — checks if the value is greater than or equal to the given minimum.min
(integer) — checks if the value is less than or equal to the given maximum.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 (it's useful to hide sensitive data).configurable
(boolean) - if false, the attribute isn't configurable from the Content Type Builder plugin.Path — User.settings.json
.
{
+ "connection": "default",
+ "info": {
+ "name": "user",
+ "description": "This represents the User Model",
+ "mainField": "email"
+ },
+ "attributes": {
+ "firstname": {
+ "type": "string"
+ },
+ "lastname": {
+ "type": "string"
+ },
+ "email": {
+ "type": "email",
+ "required": true,
+ "unique": true
+ },
+ "password": {
+ "type": "password",
+ "required": true,
+ "private": true
+ },
+ "about": {
+ "type": "description"
+ },
+ "age": {
+ "type": "integer",
+ "min": 18,
+ "max": 99
+ },
+ "birthday": {
+ "type": "date"
+ }
+ }
+}
+
Refer to the relations concept for more informations about relations type.
Refer to the one-way concept for informations.
A pet
can be owned by someone (a user
).
Path — ./api/pet/models/Pet.settings.json
.
{
+ "attributes": {
+ "owner": {
+ "model": "user"
+ }
+ }
+}
+
Path — ./api/pet/controllers/Pet.js
.
// Mongoose example
+module.exports = {
+ findPetsWithOwners: async (ctx) => {
+ // Retrieve the list of pets with their owners.
+ const pets = Pet
+ .find()
+ .populate('owner');
+
+ // Send the list of pets.
+ ctx.body = pets;
+ }
+}
+
Refer to the one-to-one concept for informations.
A user
can have one address
. And this address is only related to this user
.
Path — ./api/user/models/User.settings.json
.
{
+ "attributes": {
+ "address": {
+ "model": "address",
+ "via": "user"
+ }
+ }
+}
+
Path — ./api/address/models/Address.settings.json
.
{
+ "attributes": {
+ "user": {
+ "model": "user"
+ }
+ }
+}
+
Path — ./api/user/controllers/User.js
.
// Mongoose example
+module.exports = {
+ findUsersWithAddresses: async (ctx) => {
+ // Retrieve the list of users with their addresses.
+ const users = User
+ .find()
+ .populate('address');
+
+ // Send the list of users.
+ ctx.body = users;
+ }
+}
+
Path — ./api/adress/controllers/Address.js
.
// Mongoose example
+module.exports = {
+ findArticlesWithUsers: async (ctx) => {
+ // Retrieve the list of addresses with their users.
+ const articles = Address
+ .find()
+ .populate('user');
+
+ // Send the list of addresses.
+ ctx.body = addresses;
+ }
+}
+
Refer to the one-to-many concept for more informations.
A user
can have many articles
, and an article
can be related to one user
(author).
Path — ./api/user/models/User.settings.json
.
{
+ "attributes": {
+ "articles": {
+ "collection": "article",
+ "via": "author"
+ }
+ }
+}
+
Path — ./api/article/models/Article.settings.json
.
{
+ "attributes": {
+ "author": {
+ "model": "user"
+ }
+ }
+}
+
Path — ./api/user/controllers/User.js
.
// Mongoose example
+module.exports = {
+ findUsersWithArticles: async (ctx) => {
+ // Retrieve the list of users with their articles.
+ const users = User
+ .find()
+ .populate('articles');
+
+ // Send the list of users.
+ ctx.body = users;
+ }
+}
+
Path — ./api/article/controllers/Article.js
.
// Mongoose example
+module.exports = {
+ findArticlesWithAuthors: async (ctx) => {
+ // Retrieve the list of articles with their authors.
+ const articles = Article
+ .find()
+ .populate('author');
+
+ // Send the list of users.
+ ctx.body = users;
+ }
+}
+
Refer to the many-to-many concept.
A product
can be related to many categories
, so a category
can have many products
.
Path — ./api/product/models/Product.settings.json
.
{
+ "attributes": {
+ "categories": {
+ "collection": "category",
+ "via": "products",
+ "dominant": true
+ }
+ }
+}
+
The dominant
key allows you to define in which table/collection (only for NoSQL databases) should be stored the array that defines the relationship. Because there is no join table in NoSQL, this key is required for NoSQL databases (ex: MongoDB).
Path — ./api/category/models/Category.settings.json
.
{
+ "attributes": {
+ "products": {
+ "collection": "product",
+ "via": "categories"
+ }
+ }
+}
+
Path — ./api/product/controllers/Product.js
.
// Mongoose example
+module.exports = {
+ findProductsWithCategories: async (ctx) => {
+ // Retrieve the list of products.
+ const products = Product
+ .find()
+ .populate('categories');
+
+ // Send the list of products.
+ ctx.body = products;
+ }
+}
+
Path — ./api/category/controllers/Category.js
.
// Mongoose example
+module.exports = {
+ findCategoriesWithProducts: async (ctx) => {
+ // Retrieve the list of categories.
+ const categories = Category
+ .find()
+ .populate('products');
+
+ // Send the list of categories.
+ ctx.body = categories;
+ }
+}
+
The polymorphic relationships are the solution when you don't know which kind of model will be associated to your entry. A common use case is an Image
model that can be associated to many others kind of models (Article, Product, User, etc).
Refer to the upload plugin polymorphic implementation for more informations.
Let's stay with our Image
model which might belongs to a single Article
or Product
entry.
In other words, it means that a
Image
entry can be associated to one entry. This entry can be aArticle
orProduct
entry.
Path — ./api/image/models/Image.settings.json
.
{
+ "attributes": {
+ "related": {
+ "model": "*",
+ "filter": "field"
+ }
+ }
+}
+
Also, our Image
model which might belongs to many Article
or Product
entries.
In other words, it means that a
Article
entry can relate to the same image than aProduct
entry.
Path — ./api/image/models/Image.settings.json
.
{
+ "attributes": {
+ "related": {
+ "collection": "*",
+ "filter": "field"
+ }
+ }
+}
+
The filter
attribute is optional (but we highly recommend to use 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
.
{
+ "attributes": {
+ "cover": {
+ "model": "image",
+ "via": "related",
+ },
+ "pictures": {
+ "collection": "image",
+ "via": "related"
+ }
+ }
+}
+
The value is the filter
attribute is the name of the column where the information is stored.
A Image
model might belongs to many either Article
models or a Product
models.
Path — ./api/image/models/Image.settings.json
.
{
+ "attributes": {
+ "related": {
+ "collection": "*",
+ "filter": "field"
+ }
+ }
+}
+
Path — ./api/article/models/Article.settings.json
.
{
+ "attributes": {
+ "avatar": {
+ "model": "image",
+ "via": "related"
+ }
+ }
+}
+
Path — ./api/article/models/Product.settings.json
.
{
+ "attributes": {
+ "pictures": {
+ "collection": "image",
+ "via": "related"
+ }
+ }
+}
+
Path — ./api/image/controllers/Image.js
.
// Mongoose example
+module.exports = {
+ findFiles: async (ctx) => {
+ // Retrieve the list of images with the Article or Product entries related to them.
+ const images = Images
+ .find()
+ .populate('related');
+
+ /*
+ [{
+ "_id": "5a81b0fa8c063a53298a934a",
+ "url": "http://....",
+ "name": "john_doe_avatar.png",
+ "related": [{
+ "_id": "5a81b0fa8c063a5393qj934a",
+ "title": "John Doe is awesome",
+ "description": "..."
+ }, {
+ "_id": "5a81jei389ns5abd75f79c",
+ "name": "A simple chair",
+ "description": "..."
+ }]
+ }]
+ */
+
+ // Send the list of files.
+ ctx.body = images;
+ }
+}
+
Path — ./api/article/controllers/Article.js
.
// Mongoose example
+module.exports = {
+ findArticlesWithAvatar: async (ctx) => {
+ // Retrieve the list of articles with the avatar (image).
+ const articles = Article
+ .find()
+ .populate('avatar');
+
+ /*
+ [{
+ "_id": "5a81b0fa8c063a5393qj934a",
+ "title": "John Doe is awesome",
+ "description": "...",
+ "avatar": {
+ "_id": "5a81b0fa8c063a53298a934a",
+ "url": "http://....",
+ "name": "john_doe_avatar.png"
+ }
+ }]
+ */
+
+ // Send the list of users.
+ ctx.body = articles;
+ }
+}
+
Path — ./api/product/controllers/Product.js
.
// Mongoose example
+module.exports = {
+ findProductWithPictures: async (ctx) => {
+ // Retrieve the list of products with the pictures (images).
+ const products = Product
+ .find()
+ .populate('pictures');
+
+ /*
+ [{
+ "_id": "5a81jei389ns5abd75f79c",
+ "name": "A simple chair",
+ "description": "...",
+ "pictures": [{
+ "_id": "5a81b0fa8c063a53298a934a",
+ "url": "http://....",
+ "name": "chair_position_1.png"
+ }, {
+ "_id": "5a81d22bee1ad45abd75f79c",
+ "url": "http://....",
+ "name": "chair_position_2.png"
+ }, {
+ "_id": "5a81d232ee1ad45abd75f79e",
+ "url": "http://....",
+ "name": "chair_position_3.png"
+ }]
+ }]
+ */
+
+ // Send the list of users.
+ ctx.body = products;
+ }
+}
+
If you're using MongoDB as a database, you don't need to do anything. Everything is natively handled by Strapi. However, to implement a polymorphic relationship with SQL databases, you need to create two tables.
Path — ./api/image/models/Image.settings.json
.
{
+ "attributes": {
+ "name": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "related": {
+ "collection": "*",
+ "filter": "field"
+ }
+ }
+}
+
The first table to create is the table which has the same name as your model.
CREATE TABLE `image` (
+ `id` int(11) NOT NULL,
+ `name` text NOT NULL,
+ `text` text NOT NULL
+)
+
If you've overrided the default table name given by Strapi by using the collectionName
attribute. Use the value set in the collectionName
to name the table.
The second table will allow us to associate one or many others entries to the Image
model. The name of the table is the same as the previous one with the suffix _morph
.
CREATE TABLE `image_morph` (
+ `id` int(11) NOT NULL,
+ `image_id` int(11) NOT NULL,
+ `related_id` int(11) NOT NULL,
+ `related_type` text NOT NULL,
+ `field` text NOT NULL
+)
+
image_id
is using the name of the first table with the suffix _id
.
+Image
entry.related_id
is using the attribute name where the relation happens with the suffix _id
.
+Article
or Product
entry.related_type
is using the attribute name where the relation happens with the suffix _type
.
+Article
or Product
entry is stored.field
is using the filter property value defined in the model. If you change the filter value, you have to change the name of this column as well.
+Article
, Product
with which the Image
entry is related.id | image_id | related_id | related_type | field |
---|---|---|---|---|
1 | 1738 | 39 | product | cover |
2 | 4738 | 58 | article | avatar |
3 | 1738 | 71 | article | avatar |
Refer to the lifecycle callbacks concepts for informations.
The following events are available by default:
Callbacks on save
:
Callbacks on fetch
:
Callbacks on fetchAll
:
Callbacks on create
:
Callbacks on update
:
Callbacks on destroy
:
The entry is available through the model
parameter
Path — ./api/user/models/User.js
.
module.exports = {
+ /**
+ * Triggered before user creation.
+ */
+ beforeCreate: async (model) => {
+ // Hash password.
+ const passwordHashed = await strapi.api.user.services.user.hashPassword(model.password);
+
+ // Set the password.
+ model.password = passwordHashed;
+ }
+}
+
Each of these functions receives a three parameters model
, attrs
and options
. You have to return a Promise.
Path — ./api/user/models/User.js
.
module.exports = {
+
+ /**
+ * Triggered before user creation.
+ */
+ beforeCreate: async (model, attrs, options) => {
+ // Hash password.
+ const passwordHashed = await strapi.api.user.services.user.hashPassword(model.attributes.password);
+
+ // Set the password.
+ model.set('password', passwordHashed);
+ }
+}
+
Additional settings can be set on models:
connection
(string) - Connection's name which must be used. Default value: default
.collectionName
(string) - Collection's name (or table's name) in which the data should be stored.globalId
(string) -Global variable name for this model (case-sensitive).Path — User.settings.json
.
{
+ "connection": "mongo",
+ "collectionName": "Users_v1",
+ "globalId": "Users",
+ "attributes": {
+
+ }
+}
+
In this example, the model User
will be accessible through the Users
global variable. The data will be stored in the Users_v1
collection or table and the model will use the mongo
connection defined in ./config/environments/**/database.json
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.
+ ← + Internationalization + + Policies + → +
See the policies' concepts for details.
There are several ways to create a policy.
strapi generate:policy isAuthenticated
. Read the CLI documentation for more information.isAuthenticated.js
in ./config/policies/
.Path — ./config/policies/isAuthenticated.js
.
module.exports = async (ctx, next) => {
+ if (ctx.state.user) {
+ // Go to next policy or will reach the controller's action.
+ return await next();
+ }
+
+ ctx.unauthorized(`You're not logged in!`);
+};
+
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.
You can access to any controllers, services or models thanks to the global variable strapi
in a policy.
To apply policies to a route, you need to associate an array of policies to it. As explained in the policies' concepts, there are two kinds of policies: global or scoped.
Refer to the concept for details.
The global policies can be associated to any routes in your project.
Path — ./api/car/routes.json
.
{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/car",
+ "handler": "Car.find",
+ "config": {
+ "policies": [
+ "global.isAuthenticated"
+ ]
+ }
+ }
+ ]
+}
+
Before executing the find
action in the Car.js
controller, the global policy isAuthenticated
located in ./config/policies/isAuthenticated.js
will be called.
You can put as much policy you want in this array. However be careful about the performance impact.
Plugins can add and expose policies into your app. For example, the plugin Auth
(COMING SOON) comes with several useful policies to ensure that the user is well authenticated or has the rights to perform an action.
Path — ./api/car/config/routes.json
.
{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/car",
+ "handler": "Car.find",
+ "config": {
+ "policies": [
+ "plugins.auth.isAuthenticated"
+ ]
+ }
+ }
+ ]
+}
+
The policy isAuthenticated
located in ./plugins/auth/config/policies/isAuthenticated.js
will be executed before the find
action in the Car.js
controller.
The scoped policies can only be associated to the routes defined in the API where they have been declared.
Path — ./api/car/config/policies/isAdmin.js
.
module.exports = async (ctx, next) => {
+ if (ctx.state.user.role.name === 'Administrator') {
+ // Go to next policy or will reach the controller's action.
+ return await next();
+ }
+
+ ctx.unauthorized(`You're not allowed to perform this action!`);
+};
+
Path — ./api/car/config/routes.json
.
{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/car",
+ "handler": "Car.find",
+ "config": {
+ "policies": [
+ "isAdmin"
+ ]
+ }
+ }
+ ]
+}
+
The policy isAdmin
located in ./api/car/config/policies/isAdmin.js
will be executed before the find
action in the Car.js
controller.
The policy isAdmin
can only be applied to the routes defined in the /api/car
folder.
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
.
module.exports = async (ctx, next) => {
+ // Indicate to the server to go to
+ // the next policy or to the controller's action.
+ await next();
+
+ // The code below will be executed after the controller's action.
+ if (ctx.status === 404) {
+ ctx.body = 'We cannot find the ressource.';
+ }
+};
+
+ ← + Models + + Public Assets + → +
See the public assets concepts for details.
Because an API may need to serve static assets, every new Strapi project includes by default, a folder named /public
. Any file located in this directory is accessible if the request's path doesn't match any other defined route and if it matches a public file name.
An image named company-logo.png
in ./public/
is accessible through /company-logo.png
URL.
index.html
files are served if the request corresponds to a folder name (/pictures
url will try to serve public/pictures/index.html
file).
WARNING
The dotfiles are not exposed. It means that every files with the names start by .
such as .htaccess
or .gitignore
are not served.
Refer to the public assets configurations for more informations.
+ ← + Policies + + Request + → +
See the requests concepts for details.
The context object (ctx
) contains all the requests related informations. They are accessible through ctx.request
, from controllers and policies.
For more information, please refer to the Koa request documentation.
Request header object.
Set request header object.
Request header object. Alias as request.header
.
Set request header object. Alias as request.header=
.
Request method.
Set request method, useful for implementing middleware
+such as methodOverride()
.
Return request Content-Length as a number when present, or undefined
.
Get request URL.
Set request URL, useful for url rewrites.
Get request original URL.
Get origin of URL, include protocol
and host
.
ctx.request.origin
+// => http://example.com
+
Get full request URL, include protocol
, host
and url
.
ctx.request.href;
+// => http://example.com/foo/bar?q=1
+
Get request pathname.
Set request pathname and retain query-string when present.
Get raw query string void of ?
.
Set raw query string.
Get raw query string with the ?
.
Set raw query string.
Get host (hostname:port) when present. Supports X-Forwarded-Host
+when app.proxy
is true, otherwise Host
is used.
Get hostname when present. Supports X-Forwarded-Host
+when app.proxy
is true, otherwise Host
is used.
If host is IPv6, Koa delegates parsing to +WHATWG URL API, +Note This may impact performance.
Get WHATWG parsed URL object.
Get request Content-Type
void of parameters such as "charset".
const ct = ctx.request.type;
+// => "image/png"
+
Get request charset when present, or undefined
:
ctx.request.charset;
+// => "utf-8"
+
Get parsed query-string, returning an empty object when no +query-string is present. Note that this getter does not +support nested parsing.
For example "color=blue&size=small":
{
+ color: 'blue',
+ size: 'small'
+}
+
Set query-string to the given object. Note that this +setter does not support nested objects.
ctx.query = { next: '/login' };
+
Check if a request cache is "fresh", aka the contents have not changed. This
+method is for cache negotiation between If-None-Match
/ ETag
, and If-Modified-Since
and Last-Modified
. It should be referenced after setting one or more of these response headers.
// freshness check requires status 20x or 304
+ctx.status = 200;
+ctx.set('ETag', '123');
+
+// cache is ok
+if (ctx.fresh) {
+ ctx.status = 304;
+ return;
+}
+
+// cache is stale
+// fetch new data
+ctx.body = await db.find('something');
+
Inverse of request.fresh
.
Return request protocol, "https" or "http". Supports X-Forwarded-Proto
+when app.proxy
is true.
Shorthand for ctx.protocol == "https"
to check if a request was
+issued via TLS.
Request remote address. Supports X-Forwarded-For
when app.proxy
+is true.
When X-Forwarded-For
is present and app.proxy
is enabled an array
+of these ips is returned, ordered from upstream -> downstream. When disabled
+an empty array is returned.
Return subdomains as an array.
Subdomains are the dot-separated parts of the host before the main domain of
+the app. By default, the domain of the app is assumed to be the last two
+parts of the host. This can be changed by setting app.subdomainOffset
.
For example, if the domain is "tobi.ferrets.example.com":
+If app.subdomainOffset
is not set, ctx.subdomains
is ["ferrets", "tobi"]
.
+If app.subdomainOffset
is 3, ctx.subdomains
is ["tobi"]
.
Check if the incoming request contains the "Content-Type"
+header field, and it contains any of the give mime type
s.
+If there is no request body, null
is returned.
+If there is no content type, or the match fails false
is returned.
+Otherwise, it returns the matching content-type.
// With Content-Type: text/html; charset=utf-8
+ctx.is('html'); // => 'html'
+ctx.is('text/html'); // => 'text/html'
+ctx.is('text/*', 'text/html'); // => 'text/html'
+
+// When Content-Type is application/json
+ctx.is('json', 'urlencoded'); // => 'json'
+ctx.is('application/json'); // => 'application/json'
+ctx.is('html', 'application/*'); // => 'application/json'
+
+ctx.is('html'); // => false
+
For example if you want to ensure that +only images are sent to a given route:
if (ctx.is('image/*')) {
+// process
+} else {
+ ctx.throw(415, 'images only!');
+}
+
Koa's request
object includes helpful content negotiation utilities powered by accepts and negotiator. These utilities are:
request.accepts(types)
request.acceptsEncodings(types)
request.acceptsCharsets(charsets)
request.acceptsLanguages(langs)
If no types are supplied, all acceptable types are returned.
If multiple types are supplied, the best match will be returned. If no matches are found, a false
is returned, and you should send a 406 "Not Acceptable"
response to the client.
In the case of missing accept headers where any type is acceptable, the first type will be returned. Thus, the order of types you supply is important.
Check if the given type(s)
is acceptable, returning the best match when true, otherwise false
. The type
value may be one or more mime type string
+such as "application/json", the extension name
+such as "json", or an array ["json", "html", "text/plain"]
.
// Accept: text/html
+ctx.accepts('html');
+// => "html"
+
+// Accept: text/*, application/json
+ctx.accepts('html');
+// => "html"
+ctx.accepts('text/html');
+// => "text/html"
+ctx.accepts('json', 'text');
+// => "json"
+ctx.accepts('application/json');
+// => "application/json"
+
+// Accept: text/*, application/json
+ctx.accepts('image/png');
+ctx.accepts('png');
+// => false
+
+// Accept: text/*;q=.5, application/json
+ctx.accepts(['html', 'json']);
+ctx.accepts('html', 'json');
+// => "json"
+
+// No Accept header
+ctx.accepts('html', 'json');
+// => "html"
+ctx.accepts('json', 'html');
+// => "json"
+
You may call ctx.accepts()
as many times as you like,
+or use a switch:
switch (ctx.accepts('json', 'html', 'text')) {
+ case 'json': break;
+ case 'html': break;
+ case 'text': break;
+ default: ctx.throw(406, 'json, html, or text only');
+}
+
Check if encodings
are acceptable, returning the best match when true, otherwise false
. Note that you should include identity
as one of the encodings!
// Accept-Encoding: gzip
+ctx.acceptsEncodings('gzip', 'deflate', 'identity');
+// => "gzip"
+
+ctx.acceptsEncodings(['gzip', 'deflate', 'identity']);
+// => "gzip"
+
When no arguments are given all accepted encodings +are returned as an array:
// Accept-Encoding: gzip, deflate
+ctx.acceptsEncodings();
+// => ["gzip", "deflate", "identity"]
+
Note that the identity
encoding (which means no encoding) could be unacceptable if the client explicitly sends identity;q=0
. Although this is an edge case, you should still handle the case where this method returns false
.
Check if charsets
are acceptable, returning
+the best match when true, otherwise false
.
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
+ctx.acceptsCharsets('utf-8', 'utf-7');
+// => "utf-8"
+
+ctx.acceptsCharsets(['utf-7', 'utf-8']);
+// => "utf-8"
+
When no arguments are given all accepted charsets +are returned as an array:
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
+ctx.acceptsCharsets();
+// => ["utf-8", "utf-7", "iso-8859-1"]
+
Check if langs
are acceptable, returning
+the best match when true, otherwise false
.
// Accept-Language: en;q=0.8, es, pt
+ctx.acceptsLanguages('es', 'en');
+// => "es"
+
+ctx.acceptsLanguages(['en', 'es']);
+// => "es"
+
When no arguments are given all accepted languages +are returned as an array:
// Accept-Language: en;q=0.8, es, pt
+ctx.acceptsLanguages();
+// => ["es", "pt", "en"]
+
Check if the request is idempotent.
Return the request socket.
Return request header.
+ ← + Public Assets + + Admin panel + → +
See the responses concepts for details.
For more information, please refer to the Koa response documentation.
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.
Response header object.
Response header object. Alias as response.header
.
Request socket.
Get response status. By default, response.status
is set to 404
unlike node's res.statusCode
which defaults to 200
.
Set response status via numeric code:
NOTE: don't worry too much about memorizing these strings, +if you have a typo an error will be thrown, displaying this list +so you can make a correction.
Get response status message. By default, response.message
is
+associated with response.status
.
Set response status message to the given value.
Set response Content-Length to the given value.
Return response Content-Length as a number when present, or deduce
+from ctx.body
when possible, or undefined
.
Get response body.
Set response body to one of the following:
string
writtenBuffer
writtenStream
pipedObject
|| Array
json-stringifiednull
no content responseIf response.status
has not been set, Koa will automatically set the status to 200
or 204
.
The Content-Type is defaulted to text/html or text/plain, both with +a default charset of utf-8. The Content-Length field is also set.
The Content-Type is defaulted to application/octet-stream, and Content-Length +is also set.
The Content-Type is defaulted to application/octet-stream.
Whenever a stream is set as the response body, .onerror
is automatically added as a listener to the error
event to catch any errors.
+In addition, whenever the request is closed (even prematurely), the stream is destroyed.
+If you do not want these two features, do not set the stream as the body directly.
+For example, you may not want this when setting the body as an HTTP stream in a proxy as it would destroy the underlying connection.
See: https://github.com/koajs/koa/pull/612 for more information.
Here's an example of stream error handling without automatically destroying the stream:
const PassThrough = require('stream').PassThrough;
+
+app.use(async ctx => {
+ ctx.body = someHTTPStream.on('error', ctx.onerror).pipe(PassThrough());
+});
+
The Content-Type is defaulted to application/json. This includes plain objects { foo: 'bar' }
and arrays ['foo', 'bar']
.
Get a response header field value with case-insensitive field
.
const etag = ctx.response.get('ETag');
+
Set response header field
to value
:
ctx.set('Cache-Control', 'no-cache');
+
Append additional header field
with value val
.
ctx.append('Link', '<http://127.0.0.1/>');
+
Set several response header fields
with an object:
ctx.set({
+ 'Etag': '1234',
+ 'Last-Modified': date
+});
+
Remove header field
.
Get response Content-Type
void of parameters such as "charset".
const ct = ctx.type;
+// => "image/png"
+
Set response Content-Type
via mime string or file extension.
ctx.type = 'text/plain; charset=utf-8';
+ctx.type = 'image/png';
+ctx.type = '.png';
+ctx.type = 'png';
+
when appropriate a charset
is selected for you, for
+example response.type = 'html'
will default to "utf-8". If you need to overwrite charset
,
+use ctx.set('Content-Type', 'text/html')
to set response header field to value directly.
Very similar to ctx.request.is()
.
+Check whether the response type is one of the supplied types.
+This is particularly useful for creating middleware that
+manipulate responses.
For example, this is a middleware that minifies +all HTML responses except for streams.
const minify = require('html-minifier');
+
+app.use(async (ctx, next) => {
+await next();
+
+if (!ctx.response.is('html')) return;
+
+let body = ctx.body;
+if (!body || body.pipe) return;
+
+if (Buffer.isBuffer(body)) body = body.toString();
+ ctx.body = minify(body);
+});
+
Perform a [302] redirect to url
.
The string "back" is special-cased
+to provide Referrer support, when Referrer
+is not present alt
or "/" is used.
ctx.redirect('back');
+ctx.redirect('back', '/index.html');
+ctx.redirect('/login');
+ctx.redirect('http://google.com');
+
To alter the default status of 302
, simply assign the status
+before or after this call. To alter the body, assign it after this call:
ctx.status = 301;
+ctx.redirect('/cart');
+ctx.body = 'Redirecting to shopping cart';
+
Set Content-Disposition
to "attachment" to signal the client
+to prompt for download. Optionally specify the filename
of the
+download.
Check if a response header has already been sent. Useful for seeing +if the client may be notified on error.
Return the Last-Modified
header as a Date
, if it exists.
Set the Last-Modified
header as an appropriate UTC string.
+You can either set it as a Date
or date string.
ctx.response.lastModified = new Date();
+
Set the ETag of a response including the wrapped "
s.
+Note that there is no corresponding response.etag
getter.
ctx.response.etag = crypto.createHash('md5').update(ctx.body).digest('hex');
+
Vary on field
.
Flush any set headers, and begin the body.
A Koa Response
object is an abstraction on top of node's vanilla response object,
+providing additional functionality that is useful for every day HTTP server
+development.
Response header object.
Response header object. Alias as response.header
.
Request socket.
Get response status. By default, response.status
is set to 404
unlike node's res.statusCode
which defaults to 200
.
Set response status via numeric code:
NOTE: don't worry too much about memorizing these strings, +if you have a typo an error will be thrown, displaying this list +so you can make a correction.
Get response status message. By default, response.message
is
+associated with response.status
.
Set response status message to the given value.
Set response Content-Length to the given value.
Return response Content-Length as a number when present, or deduce
+from ctx.body
when possible, or undefined
.
Get response body.
Set response body to one of the following:
string
writtenBuffer
writtenStream
pipedObject
|| Array
json-stringifiednull
no content responseIf response.status
has not been set, Koa will automatically set the status to 200
or 204
.
The Content-Type is defaulted to text/html or text/plain, both with +a default charset of utf-8. The Content-Length field is also set.
The Content-Type is defaulted to application/octet-stream, and Content-Length +is also set.
The Content-Type is defaulted to application/octet-stream.
Whenever a stream is set as the response body, .onerror
is automatically added as a listener to the error
event to catch any errors.
+In addition, whenever the request is closed (even prematurely), the stream is destroyed.
+If you do not want these two features, do not set the stream as the body directly.
+For example, you may not want this when setting the body as an HTTP stream in a proxy as it would destroy the underlying connection.
See: https://github.com/koajs/koa/pull/612 for more information.
Here's an example of stream error handling without automatically destroying the stream:
const PassThrough = require('stream').PassThrough;
+
+app.use(async ctx => {
+ ctx.body = someHTTPStream.on('error', ctx.onerror).pipe(PassThrough());
+});
+
The Content-Type is defaulted to application/json. This includes plain objects { foo: 'bar' }
and arrays ['foo', 'bar']
.
Get a response header field value with case-insensitive field
.
const etag = ctx.response.get('ETag');
+
Set response header field
to value
:
ctx.set('Cache-Control', 'no-cache');
+
Append additional header field
with value val
.
ctx.append('Link', '<http://127.0.0.1/>');
+
Set several response header fields
with an object:
ctx.set({
+ 'Etag': '1234',
+ 'Last-Modified': date
+});
+
Remove header field
.
Get response Content-Type
void of parameters such as "charset".
const ct = ctx.type;
+// => "image/png"
+
Set response Content-Type
via mime string or file extension.
ctx.type = 'text/plain; charset=utf-8';
+ctx.type = 'image/png';
+ctx.type = '.png';
+ctx.type = 'png';
+
when appropriate a charset
is selected for you, for
+example response.type = 'html'
will default to "utf-8". If you need to overwrite charset
,
+use ctx.set('Content-Type', 'text/html')
to set response header field to value directly.
Very similar to ctx.request.is()
.
+Check whether the response type is one of the supplied types.
+This is particularly useful for creating middleware that
+manipulate responses.
For example, this is a middleware that minifies +all HTML responses except for streams.
const minify = require('html-minifier');
+
+app.use(async (ctx, next) => {
+await next();
+
+if (!ctx.response.is('html')) return;
+
+let body = ctx.body;
+if (!body || body.pipe) return;
+
+if (Buffer.isBuffer(body)) body = body.toString();
+ ctx.body = minify(body);
+});
+
Perform a [302] redirect to url
.
The string "back" is special-cased
+to provide Referrer support, when Referrer
+is not present alt
or "/" is used.
ctx.redirect('back');
+ctx.redirect('back', '/index.html');
+ctx.redirect('/login');
+ctx.redirect('http://google.com');
+
To alter the default status of 302
, simply assign the status
+before or after this call. To alter the body, assign it after this call:
ctx.status = 301;
+ctx.redirect('/cart');
+ctx.body = 'Redirecting to shopping cart';
+
Set Content-Disposition
to "attachment" to signal the client
+to prompt for download. Optionally specify the filename
of the
+download.
Check if a response header has already been sent. Useful for seeing +if the client may be notified on error.
Return the Last-Modified
header as a Date
, if it exists.
Set the Last-Modified
header as an appropriate UTC string.
+You can either set it as a Date
or date string.
ctx.response.lastModified = new Date();
+
Set the ETag of a response including the wrapped "
s.
+Note that there is no corresponding response.etag
getter.
ctx.response.etag = crypto.createHash('md5').update(ctx.body).digest('hex');
+
Vary on field
.
Flush any set headers, and begin the body.
Strapi integrates Boom: a set of utilities for returning HTTP errors. Every Boom’s functions are accessible through the ctx.response
.
You can also override responses based on them status. Please read the configuration responses for that.
Every Boom's functions is delegated to the context. It means that ctx.notFound
is a shortcut to ctx.response.notFound
.
For more information, please refer to the Boom documentation.
ctx.response.badRequest([message], [data])
ctx.response.unauthorized([message], [scheme], [attributes])
ctx.response.paymentRequired([message], [data])
ctx.response.forbidden([message], [data])
ctx.response.notFound([message], [data])
ctx.response.methodNotAllowed([message], [data], [allow])
ctx.response.notAcceptable([message], [data])
ctx.response.proxyAuthRequired([message], [data])
ctx.response.clientTimeout([message], [data])
ctx.response.conflict([message], [data])
ctx.response.resourceGone([message], [data])
ctx.response.lengthRequired([message], [data])
ctx.response.preconditionFailed([message], [data])
ctx.response.entityTooLarge([message], [data])
ctx.response.uriTooLong([message], [data])
ctx.response.unsupportedMediaType([message], [data])
ctx.response.rangeNotSatisfiable([message], [data])
ctx.response.expectationFailed([message], [data])
ctx.response.teapot([message], [data])
ctx.response.badData([message], [data])
ctx.response.locked([message], [data])
ctx.response.preconditionRequired([message], [data])
ctx.response.tooManyRequests([message], [data])
ctx.response.illegal([message], [data])
ctx.response.badRequest([message], [data])
Returns a 400 Bad Request error where:
message
- optional message.data
- optional additional error data.ctx.response.badRequest('invalid query');
+
Generates the following response payload:
{
+ "statusCode": 400,
+ "error": "Bad Request",
+ "message": "invalid query"
+}
+
ctx.response.unauthorized([message], [scheme], [attributes])
Returns a 401 Unauthorized error where:
message
- optional message.scheme
can be one of the following:
+attributes
- an object of values to use while setting the 'WWW-Authenticate' header. This value is only used
+when scheme
is a string, otherwise it is ignored. Every key/value pair will be included in the
+'WWW-Authenticate' in the format of 'key="value"' as well as in the response payload under the attributes
key. Alternatively value can be a string which is use to set the value of the scheme, for example setting the token value for negotiate header. If string is used message parameter must be null.
+null
and undefined
will be replaced with an empty string. If attributes
is set, message
will be used as
+the 'error' segment of the 'WWW-Authenticate' header. If message
is unset, the 'error' segment of the header
+will not be present and isMissing
will be true on the error object.If either scheme
or attributes
are set, the resultant Boom
object will have the 'WWW-Authenticate' header set for the response.
ctx.response.unauthorized('invalid password');
+
Generates the following response:
"payload": {
+ "statusCode": 401,
+ "error": "Unauthorized",
+ "message": "invalid password"
+},
+"headers" {}
+
ctx.response.unauthorized('invalid password', 'sample');
+
Generates the following response:
"payload": {
+ "statusCode": 401,
+ "error": "Unauthorized",
+ "message": "invalid password",
+ "attributes": {
+ "error": "invalid password"
+ }
+},
+"headers" {
+ "WWW-Authenticate": "sample error=\"invalid password\""
+}
+
ctx.response.unauthorized(null, 'Negotiate', 'VGhpcyBpcyBhIHRlc3QgdG9rZW4=');
+
Generates the following response:
"payload": {
+ "statusCode": 401,
+ "error": "Unauthorized",
+ "attributes": "VGhpcyBpcyBhIHRlc3QgdG9rZW4="
+},
+"headers" {
+ "WWW-Authenticate": "Negotiate VGhpcyBpcyBhIHRlc3QgdG9rZW4="
+}
+
ctx.response.unauthorized('invalid password', 'sample', { ttl: 0, cache: null, foo: 'bar' });
+
Generates the following response:
"payload": {
+ "statusCode": 401,
+ "error": "Unauthorized",
+ "message": "invalid password",
+ "attributes": {
+ "error": "invalid password",
+ "ttl": 0,
+ "cache": "",
+ "foo": "bar"
+ }
+},
+"headers" {
+ "WWW-Authenticate": "sample ttl=\"0\", cache=\"\", foo=\"bar\", error=\"invalid password\""
+}
+
ctx.response.paymentRequired([message], [data])
Returns a 402 Payment Required error where:
message
- optional message.data
- optional additional error data.ctx.response.paymentRequired('bandwidth used');
+
Generates the following response payload:
{
+ "statusCode": 402,
+ "error": "Payment Required",
+ "message": "bandwidth used"
+}
+
ctx.response.forbidden([message], [data])
Returns a 403 Forbidden error where:
message
- optional message.data
- optional additional error data.ctx.response.forbidden('try again some time');
+
Generates the following response payload:
{
+ "statusCode": 403,
+ "error": "Forbidden",
+ "message": "try again some time"
+}
+
ctx.response.notFound([message], [data])
Returns a 404 Not Found error where:
message
- optional message.data
- optional additional error data.ctx.response.notFound('missing');
+
Generates the following response payload:
{
+ "statusCode": 404,
+ "error": "Not Found",
+ "message": "missing"
+}
+
ctx.response.methodNotAllowed([message], [data], [allow])
Returns a 405 Method Not Allowed error where:
message
- optional message.data
- optional additional error data.allow
- optional string or array of strings (to be combined and separated by ', ') which is set to the 'Allow' header.ctx.response.methodNotAllowed('that method is not allowed');
+
Generates the following response payload:
{
+ "statusCode": 405,
+ "error": "Method Not Allowed",
+ "message": "that method is not allowed"
+}
+
ctx.response.notAcceptable([message], [data])
Returns a 406 Not Acceptable error where:
message
- optional message.data
- optional additional error data.ctx.response.notAcceptable('unacceptable');
+
Generates the following response payload:
{
+ "statusCode": 406,
+ "error": "Not Acceptable",
+ "message": "unacceptable"
+}
+
ctx.response.proxyAuthRequired([message], [data])
Returns a 407 Proxy Authentication Required error where:
message
- optional message.data
- optional additional error data.ctx.response.proxyAuthRequired('auth missing');
+
Generates the following response payload:
{
+ "statusCode": 407,
+ "error": "Proxy Authentication Required",
+ "message": "auth missing"
+}
+
ctx.response.clientTimeout([message], [data])
Returns a 408 Request Time-out error where:
message
- optional message.data
- optional additional error data.ctx.response.clientTimeout('timed out');
+
Generates the following response payload:
{
+ "statusCode": 408,
+ "error": "Request Time-out",
+ "message": "timed out"
+}
+
ctx.response.conflict([message], [data])
Returns a 409 Conflict error where:
message
- optional message.data
- optional additional error data.ctx.response.conflict('there was a conflict');
+
Generates the following response payload:
{
+ "statusCode": 409,
+ "error": "Conflict",
+ "message": "there was a conflict"
+}
+
ctx.response.resourceGone([message], [data])
Returns a 410 Gone error where:
message
- optional message.data
- optional additional error data.ctx.response.resourceGone('it is gone');
+
Generates the following response payload:
{
+ "statusCode": 410,
+ "error": "Gone",
+ "message": "it is gone"
+}
+
ctx.response.lengthRequired([message], [data])
Returns a 411 Length Required error where:
message
- optional message.data
- optional additional error data.ctx.response.lengthRequired('length needed');
+
Generates the following response payload:
{
+ "statusCode": 411,
+ "error": "Length Required",
+ "message": "length needed"
+}
+
ctx.response.preconditionFailed([message], [data])
Returns a 412 Precondition Failed error where:
message
- optional message.data
- optional additional error data.ctx.response.preconditionFailed();
+
Generates the following response payload:
{
+ "statusCode": 412,
+ "error": "Precondition Failed"
+}
+
ctx.response.entityTooLarge([message], [data])
Returns a 413 Request Entity Too Large error where:
message
- optional message.data
- optional additional error data.ctx.response.entityTooLarge('too big');
+
Generates the following response payload:
{
+ "statusCode": 413,
+ "error": "Request Entity Too Large",
+ "message": "too big"
+}
+
ctx.response.uriTooLong([message], [data])
Returns a 414 Request-URI Too Large error where:
message
- optional message.data
- optional additional error data.ctx.response.uriTooLong('uri is too long');
+
Generates the following response payload:
{
+ "statusCode": 414,
+ "error": "Request-URI Too Large",
+ "message": "uri is too long"
+}
+
ctx.response.unsupportedMediaType([message], [data])
Returns a 415 Unsupported Media Type error where:
message
- optional message.data
- optional additional error data.ctx.response.unsupportedMediaType('that media is not supported');
+
Generates the following response payload:
{
+ "statusCode": 415,
+ "error": "Unsupported Media Type",
+ "message": "that media is not supported"
+}
+
ctx.response.rangeNotSatisfiable([message], [data])
Returns a 416 Requested Range Not Satisfiable error where:
message
- optional message.data
- optional additional error data.ctx.response.rangeNotSatisfiable();
+
Generates the following response payload:
{
+ "statusCode": 416,
+ "error": "Requested Range Not Satisfiable"
+}
+
ctx.response.expectationFailed([message], [data])
Returns a 417 Expectation Failed error where:
message
- optional message.data
- optional additional error data.ctx.response.expectationFailed('expected this to work');
+
Generates the following response payload:
{
+ "statusCode": 417,
+ "error": "Expectation Failed",
+ "message": "expected this to work"
+}
+
ctx.response.teapot([message], [data])
Returns a 418 I'm a Teapot error where:
message
- optional message.data
- optional additional error data.ctx.response.teapot('sorry, no coffee...');
+
Generates the following response payload:
{
+ "statusCode": 418,
+ "error": "I'm a Teapot",
+ "message": "Sorry, no coffee..."
+}
+
ctx.response.badData([message], [data])
Returns a 422 Unprocessable Entity error where:
message
- optional message.data
- optional additional error data.ctx.response.badData('your data is bad and you should feel bad');
+
Generates the following response payload:
{
+ "statusCode": 422,
+ "error": "Unprocessable Entity",
+ "message": "your data is bad and you should feel bad"
+}
+
ctx.response.locked([message], [data])
Returns a 423 Locked error where:
message
- optional message.data
- optional additional error data.ctx.response.locked('this resource has been locked');
+
Generates the following response payload:
{
+ "statusCode": 423,
+ "error": "Locked",
+ "message": "this resource has been locked"
+}
+
ctx.response.preconditionRequired([message], [data])
Returns a 428 Precondition Required error where:
message
- optional message.data
- optional additional error data.ctx.response.preconditionRequired('you must supply an If-Match header');
+
Generates the following response payload:
{
+ "statusCode": 428,
+ "error": "Precondition Required",
+ "message": "you must supply an If-Match header"
+}
+
ctx.response.tooManyRequests([message], [data])
Returns a 429 Too Many Requests error where:
message
- optional message.data
- optional additional error data.ctx.response.tooManyRequests('you have exceeded your request limit');
+
Generates the following response payload:
{
+ "statusCode": 429,
+ "error": "Too Many Requests",
+ "message": "you have exceeded your request limit"
+}
+
ctx.response.illegal([message], [data])
Returns a 451 Unavailable For Legal Reasons error where:
message
- optional message.data
- optional additional error data.ctx.response.illegal('you are not permitted to view this resource for legal reasons');
+
Generates the following response payload:
{
+ "statusCode": 451,
+ "error": "Unavailable For Legal Reasons",
+ "message": "you are not permitted to view this resource for legal reasons"
+}
+
All 500 errors hide your message from the end user. Your message is recorded in the server log.
ctx.response.badImplementation([message], [data])
- (alias: internal
)Returns a 500 Internal Server Error error where:
message
- optional message.data
- optional additional error data.ctx.response.badImplementation('terrible implementation');
+
Generates the following response payload:
{
+ "statusCode": 500,
+ "error": "Internal Server Error",
+ "message": "An internal server error occurred"
+}
+
ctx.response.notImplemented([message], [data])
Returns a 501 Not Implemented error where:
message
- optional message.data
- optional additional error data.ctx.response.notImplemented('method not implemented');
+
Generates the following response payload:
{
+ "statusCode": 501,
+ "error": "Not Implemented",
+ "message": "method not implemented"
+}
+
ctx.response.badGateway([message], [data])
Returns a 502 Bad Gateway error where:
message
- optional message.data
- optional additional error data.ctx.response.badGateway('that is a bad gateway');
+
Generates the following response payload:
{
+ "statusCode": 502,
+ "error": "Bad Gateway",
+ "message": "that is a bad gateway"
+}
+
ctx.response.serverUnavailable([message], [data])
Returns a 503 Service Unavailable error where:
message
- optional message.data
- optional additional error data.ctx.response.serverUnavailable('unavailable');
+
Generates the following response payload:
{
+ "statusCode": 503,
+ "error": "Service Unavailable",
+ "message": "unavailable"
+}
+
ctx.response.gatewayTimeout([message], [data])
Returns a 504 Gateway Time-out error where:
message
- optional message.data
- optional additional error data.ctx.response.gatewayTimeout();
+
Generates the following response payload:
{
+ "statusCode": 504,
+ "error": "Gateway Time-out"
+}
+
See the filters' concepts for details.
by default, the filters can only be used from find
endpoints generated by the Content Type Builder and the CLI. If you need to implement a filters system somewhere else, read the programmatic usage section.
The available operators are separated in four different categories:
Easily filter results according to fields values.
=
: Equals_ne
: Not equals_lt
: Lower than_gt
: Greater than_lte
: Lower than or equal to_gte
: Greater than or equal to_contains
: Contains_containss
: Contains case sensitiveFind users having John
as first name.
GET /user?firstName=John
Find products having a price equal or greater than 3
.
GET /product?price_gte=3
Sort according to a specific field.
Sort users by email.
GET /user?_sort=email:asc
GET /user?_sort=email:desc
Limit the size of the returned results.
Limit the result length to 30.
GET /user?_limit=30
Skip a specific number of entries (especially useful for pagination).
Get the second page of results.
GET /user?_start=10&_limit=10
Requests system can be implemented in custom code sections.
To extract the filters from an JavaScript object or a request, you need to call the strapi.utils.models.convertParams
helper.
The returned objects is formatted according to the ORM used by the model.
Path — ./api/user/controllers/User.js
.
// Define a list of params.
+const params = {
+ '_limit': 20,
+ '_sort': 'email'
+};
+
+// Convert params.
+const formattedParams = strapi.utils.models.convertParams('user', params); // { limit: 20, sort: 'email' }
+
Path — ./api/user/controllers/User.js
.
module.exports = {
+
+ find: async (ctx) => {
+ // Convert params.
+ const formattedParams = strapi.utils.models.convertParams('user', ctx.request.query);
+
+ // Get the list of users according to the request query.
+ const filteredUsers = await User
+ .find()
+ .where(formattedParams.where)
+ .sort(formattedParams.sort)
+ .skip(formattedParams.start)
+ .limit(formattedParams.limit);
+
+ // Finally, send the results to the client.
+ ctx.body = filteredUsers;
+ };
+};
+
See the routing's concept for details.
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
.
{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/product",
+ "handler": "Product.find",
+ },
+ {
+ "method": ["POST", "PUT"],
+ "path": "/product/:id",
+ "handler": "Product.createOrUpdate",
+ },
+ {
+ "method": "POST",
+ "path": "/product/:id/buy",
+ "handler": "Product.buy",
+ "config": {
+ "policies": ["isAuthenticated", "hasCreditCard"]
+ }
+ }
+ ]
+}
+
method
(string): Method or array of methods to hit the route (ex: GET
, POST
, PUT
, HEAD
, DELETE
, PATCH
)path
(string): URL starting with /
(ex: /product
)handler
(string): Action to executed when the route is hit following this syntax <Controller>.<action>
config
policies
(array): Array of policies names or path (see more)prefix
(string): Set a prefix to this route. Also, it will be loaded into the main router (useful feature for plugin)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 PathToRegex documentation.
{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/product/:category/:id",
+ "handler": "Product.findOneByCategory",
+ },
+ {
+ "method": "GET",
+ "path": "/product/:region(\\d{2}|\\d{3})/:id", // Only match when the first parameter contains 2 or 3 digits.
+ "handler": "Product.findOneByRegion",
+ }
+ ]
+}
+
By default, the main route of the server /
is pointed to the /public/index.html
file. To override this behavior, you need to create a route with an empty path /
in one of your API folder (/api/**/config/routes.json
).
{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/",
+ "handler": "Controller.name",
+ }
+ ]
+}
+
See the services concept for details.
There is two ways to create a service.
strapi generate:service user
. Read the CLI documentation for more information.User.js
in ./api/**/services/
.The goal of a service is to store reusable functions. An email
service could be useful, if we plan to send emails from different functions in our codebase:
Path — ./api/email/services/Email.js
.
const nodemailer = require('nodemailer');
+
+// Create reusable transporter object using SMTP transport.
+const transporter = nodemailer.createTransport({
+ service: 'Gmail',
+ auth: {
+ user: 'user@gmail.com',
+ pass: 'password'
+ }
+});
+
+module.exports = {
+ send: (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);
+ }
+};
+
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
.
module.exports = {
+ // GET /hello
+ signup: async (ctx) => {
+ // Store the new user in database.
+ const user = await User.create(ctx.params);
+
+ // Send an email to validate his subscriptions.
+ strapi.services.email.send('welcome@mysite.com', user.email, 'Welcome', '...');
+
+ // Send response to the server.
+ ctx.send({
+ ok: true
+ });
+ }
+};
+
WARNING
This feature requires the Upload plugin (installed by default).
Thanks to the plugin Upload
, you can upload any kind of files on your server or externals providers such as AWS S3.
The plugin exposes a single route POST /upload
to upload one or multiple files in a single request.
WARNING
Please send the request using multipart/form-data encoding
Parameters
files
: The file(s) to upload. The value(s) can be a Buffer or Stream.refId
: (optional): The ID of the entry which the file(s) will be linked to.ref
: (optional): The name 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
: (optional): The field of the entry which the file(s) will be precisely linked to.To add a new file attribute in your models, it's like adding a new association. In the first example, you will be able to upload and attach one file to the avatar attribute. Whereas, in our second example, you can upload and attach multiple pictures to the product.
Path — User.settings.json
.
{
+ "connection": "default",
+ "attributes": {
+ "pseudo": {
+ "type": "string",
+ "required": true
+ },
+ "email": {
+ "type": "email",
+ "required": true,
+ "unique": true
+ },
+ "avatar": {
+ "model": "file",
+ "via": "related",
+ "plugin": "upload"
+ }
+ }
+}
+
Path — Product.settings.json
.
{
+ "connection": "default",
+ "attributes": {
+ "name": {
+ "type": "string",
+ "required": true
+ },
+ "price": {
+ "type": "integer",
+ "required": true
+ },
+ "pictures": {
+ "collection": "file",
+ "via": "related",
+ "plugin": "upload"
+ }
+ }
+}
+
Single file
curl -X POST -F 'files=@/path/to/pictures/file.jpg' http://localhost:1337/upload
+
Multiple files
curl -X POST -F 'files[]=@/path/to/pictures/fileX.jpg' -F 'files[]=@/path/to/pictures/fileY.jpg' http://localhost:1337/upload
+
Linking files to an entry
Let's say that you want to have a User
model provided by the plugin Users & Permissions
and you want to upload an avatar for a specific user.
{
+ "connection": "default",
+ "attributes": {
+ "pseudo": {
+ "type": "string",
+ "required": true
+ },
+ "email": {
+ "type": "email",
+ "required": true,
+ "unique": true
+ },
+ "avatar": {
+ "model": "file",
+ "via": "related",
+ "plugin": "upload"
+ }
+ }
+}
+
{
+ "files": "...", // Buffer or stream of file(s)
+ "refId": "5a993616b8e66660e8baf45c", // User's Id.
+ "ref": "user", // Model name.
+ "source": "users-permissions", // Plugin name.
+ "field": "avatar" // Field name in the User model.
+}
+
Here the request to make to associate the file (/path/to/pictures/avatar.jpg) to the user (id: 5a993616b8e66660e8baf45c) when the User
model is provided by the Users & Permissions
plugin.
curl -X POST -F 'files=@/path/to/pictures/avatar.jpg&refId=5a993616b8e66660e8baf45c&ref=user&source=users-permissions&field=avatar' http://localhost:1337/upload
+
By default Strapi provides a local file upload system. You might want to upload your files on AWS S3 or another provider.
To install a new provider run:
$ npm install strapi-upload-aws-s3@alpha --save
+
We have two providers available strapi-upload-aws-s3
and strapi-upload-cloudinary
, use the alpha tag to install one of them. Then, visit /admin/plugins/upload/configurations/development
on your web browser and configure the provider.
If you want to create your own, make sure the name starts with strapi-upload-
(duplicating an existing one will be easier to create), modify the auth
config object and customize the upload
and delete
functions.
Check all community providers available on npmjs.org - Providers list
The most advanced open-source Content Management Framework to build powerful API with no effort.
We've been working on a major update for Strapi during the past months, rewriting the core framework and the dashboard.
This documentation is only related to Strapi v3@alpha.13 (v1 documentation is still available).
Get Started
+Learn how to install Strapi and start developing your API.
Command Line Interface
+Get to know our CLI to make features the hacker way!
Concepts
+Get to know more about Strapi and how it works.
Guides
+Get familiar with Strapi. Discover concrete examples on how to develop the features you need.
Plugin Development
+Understand how to develop your own plugin.
API Reference
+Learn about Strapi's API, the strapi
object that is available in your backend.
Here are the major changes:
Feel free to join us on Slack and ask questions about the migration process.
Install Strapi alpha.11.1
globally on your computer. To do so run npm install strapi@3.0.0-alpha.11.1 -g
.
When it's done, generate a new empty project strapi new myNewProject
(don't pay attention to the database configuration).
You will have to update just 1 file: package.json
3.0.0-alpha.11.1
version) in package.json
file{
+ "dependencies": {
+ "lodash": "4.x.x",
+ "strapi": "3.0.0-alpha.11.1",
+ "strapi-mongoose": "3.0.0-alpha.11.1"
+ }
+}
+
Delete your old admin folder and replace it by the new one.
Copy this file /plugins/users-permissions/config/jwt.json
from your old project and paste it in the corresponding one in your new project.
Copy the fields and relations you had in your /plugins/users-permissions/models/User.settings.json
file in the new one.
Then, delete your old plugins
folder and replace it by the new one.
Now let's update the dependencies in your package.json
we edited earlier. Simply run npm install
:
That's all, you have now upgraded to Strapi alpha.11
.
This migration guide is a mix of migrations from 3.0.0-alpha.11.1 to 3.0.0-alpha.11.2, 3.0.0-alpha.11.2 to 3.0.0-alpha.11.3 and from 3.0.0-alpha.11.3 to 3.0.0-alpha.12.1.3.
Feel free to join us on Slack and ask questions about the migration process.
Install Strapi alpha.12.1.3
globally on your computer. To do so run npm install strapi@3.0.0-alpha.12.1.3 -g
.
When it's done, generate a new empty project strapi new myNewProject
(don't pay attention to the database configuration).
You will have to update just 1 file: package.json
3.0.0-alpha.12.1.3
version) in package.json
file{
+ "dependencies": {
+ "lodash": "4.x.x",
+ "strapi": "3.0.0-alpha.12.1.3",
+ "strapi-mongoose": "3.0.0-alpha.12"
+ }
+}
+
Delete your old admin folder and replace it by the new one.
Copy the fields and relations you had in your /plugins/users-permissions/models/User.settings.json
file in the new one.
Then, delete your old plugins
folder and replace it by the new one.
This update is if you come from version before alpha-11.2
Update type
of Guest
role to public
in your database. You can also update name and description:
{
+ "name": "Public",
+ "description": "Default role given to unauthenticated user.",
+ "type": "public"
+}
+
Create Authenticated role:
{
+ "name": "Authenticated",
+ "description": "Default role given to authenticated user.",
+ "type": "authenticated"
+}
+
In Users & Permissions > Advanced
in admin panel update default role to Authenticated
You also will have to reset your roles permissions.
WARNING
This update is if you come from version before alpha-11.3
You will have to replace your fetchAll
services queries of your generated API:
_.forEach(convertedParams.where, (where, key) => {
+ if (_.isArray(where.value)) {
+ for (const value in where.value) {
+ qb[value ? 'where' : 'orWhere'](key, where.symbol, where.value[value])
+ }
+ } else {
+ qb.where(key, where.symbol, where.value);
+ }
+ });
+
+ if (convertedParams.sort) {
+ qb.orderBy(convertedParams.sort.key, convertedParams.sort.order);
+ }
+
+ qb.offset(convertedParams.start);
+
+ qb.limit(convertedParams.limit);
+
That's all, you have now upgraded to Strapi alpha.12.1.3
.
Here are the major changes:
Feel free to join us on Slack and ask questions about the migration process.
Install Strapi alpha.8
globally on your computer. To do so run npm install strapi@3.0.0-alpha.8 -g
.
When it's done, generate a new empty project strapi new myNewProject
(don't pay attention to the database configuration).
You will have to update just 1 file: package.json
setup
line has changed){
+ "scripts": {
+ "setup": "cd admin && npm run setup",
+ "start": "node server.js",
+ "strapi": "node_modules/strapi/bin/strapi.js",
+ "lint": "node_modules/.bin/eslint api/**/*.js config/**/*.js plugins/**/*.js",
+ "postinstall": "node node_modules/strapi/lib/utils/post-install.js"
+ }
+}
+
3.0.0-alpha.8
version){
+ "dependencies": {
+ "lodash": "4.x.x",
+ "strapi": "3.0.0-alpha.8",
+ "strapi-mongoose": "3.0.0-alpha.8"
+ }
+}
+
Delete your old admin folder and replace by the new one.
Copy these 3 files /plugins/users-permissions/config/jwt.json
, /plugins/users-permissions/config/roles.json
and /plugins/users-permissions/models/User.settings.json
from your old project and paste them in the corresponding ones in your new project. It is important to save these files.
Then, delete your old plugins
folder and replace it by the new one.
That's all, you have now upgraded to Strapi alpha.8
.
Here are the major changes:
Feel free to join us on Slack and ask questions about the migration process.
Install Strapi alpha.9
globally on your computer. To do so run npm install strapi@3.0.0-alpha.9 -g
.
When it's done, generate a new empty project strapi new myNewProject
(don't pay attention to the database configuration).
You will have to update just 2 files: package.json
and request.json
3.0.0-alpha.9
version) in package.json
file{
+ "dependencies": {
+ "lodash": "4.x.x",
+ "strapi": "3.0.0-alpha.9",
+ "strapi-mongoose": "3.0.0-alpha.9"
+ }
+}
+
session.enabled
settings to true
in each environment file: /configs/environments/***/request.json
{
+ "session": {
+ "enabled": true
+ }
+}
+
Delete your old admin folder and replace it by the new one.
Copy this file /plugins/users-permissions/config/jwt.json
from your old project and paste it in the corresponding one in your new project.
Copy the fields and relations you had in your /plugins/users-permissions/models/User.settings.json
file in the new one.
Then, delete your old plugins
folder and replace it by the new one.
Roles are now stored in your database. You will have to re-create and configure them via the admin dashboard.
If you have an existing set of users in your database you will have to rename the collection/table from user
to users-permissions_user
.
Then update all your users by changing the old role id by the new one which is in users-permissions_role
collection/table.
That's all, you have now upgraded to Strapi alpha.9
.
Here are the major changes:
Feel free to join us on Slack and ask questions about the migration process.
Install Strapi alpha.10.1
globally on your computer. To do so run npm install strapi@3.0.0-alpha.10.1 -g
.
When it's done, generate a new empty project strapi new myNewProject
(don't pay attention to the database configuration).
You will have to update just 1 file: package.json
3.0.0-alpha.10.1
version) in package.json
file{
+ "dependencies": {
+ "lodash": "4.x.x",
+ "strapi": "3.0.0-alpha.10.1",
+ "strapi-mongoose": "3.0.0-alpha.10.1"
+ }
+}
+
Delete your old admin folder and replace it by the new one.
Copy this file /plugins/users-permissions/config/jwt.json
from your old project and paste it in the corresponding one in your new project.
Copy the fields and relations you had in your /plugins/users-permissions/models/User.settings.json
file in the new one.
Then, delete your old plugins
folder and replace it by the new one.
To let you update your configurations when your application is deployed on multiple server instances, we have created a data store for settings. So we moved all the users-permissions
plugin's configs in database.
You will have to reconfigure all your users-permissions
configs from the admin panel. Then delete the advanced.json
, email.json
and grant.json
files from plugins/users-permissions/config
folder.
We fixed how mongoose handles the model's Number
type. Previously, mongoose stored Number
type as String
and now it's Integer
. So you will have to update all your documents which have a type Number
in its model and replace their String
value with a Number
one.
That's all, you have now upgraded to Strapi alpha.10
.
To be honest with all of you, the migration process won't be easy. The new version introduces a lot of breaking changes especially on the query part. Some features are still not available for the moment such as the authentication, users & permissions, email, media upload and GraphQL. If you're using one of theses features, you shouldn't be able to migrate unless you rewrite these features from scratch.
Here are the major changes:
Feel free to join us on Slack and ask questions about the migration process.
The best way to migrate your project is to generate a new empty project using the v3. Then, copy and paste your v1-app/api
folder to the new app v3-app/api
. The next step is to follow the categories one by one in order and you will be able to migrate your project without issues.
See the Quick start section to install the latest version of Strapi.
The structure of the configurations has been harmonised and simplified. Files has been renamed or deleted, and some others has been added.
./config/general.json
renamed ./config/application.json
./config/i18n.json
renamed ./config/language.json
./config/globals.json
removed./config/studio.json
removed./config/middleware.json
added./config/hook.json
added./config/custom.json
added./config/environments/**/databases.json
renamed ./config/environments/**/database.json
./config/environments/**/response.json
added./config/environments/**/custom.json
addedPlease refer to the new documentation to set the correct values in each file.
Don't forget that middlewares has been removed. Please refers to the right section of this document for more details.
The format of the routes has changed to easily integrate multiple strategies to hit the controllers' actions. It means that the routes are not REST-limited.
{
+ "routes": {
+ "GET /post": {
+ "controller": "Post",
+ "action": "find"
+ },
+ "GET /post/:id": {
+ "controller": "Post",
+ "action": "findOne"
+ },
+ "POST /post": {
+ "controller": "Post",
+ "action": "create"
+ },
+ "PUT /post/:id": {
+ "controller": "Post",
+ "action": "update"
+ },
+ "DELETE /post/:id": {
+ "controller": "Post",
+ "action": "delete"
+ }
+ }
+}
+
{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/post",
+ "handler": "Post.find",
+ },
+ {
+ "method": "GET",
+ "path": "/post/:id",
+ "handler": "Post.findOne",
+ },
+ {
+ "method": "POST",
+ "path": "/post",
+ "handler": "Post.create",
+ },
+ {
+ "method": "PUT",
+ "path": "/post/:id",
+ "handler": "Post.update",
+ },
+ {
+ "method": "DELETE",
+ "path": "/post/:id",
+ "handler": "Post.delete",
+ }
+ ]
+}
+
Koa@1.x.x was based on generators whereas Koa@2.x.x is based on async functions. It means that the yield
word has been replaced by the await
word. Then the context
was passed via the function context itself. Now, the context
is passed through the function's parameters. Also, you don't need to apply the try/catch
pattern in each controller's actions.
module.exports = {
+ find: function *() {
+ try {
+ this.body = yield Post.find(this.params);
+ } catch (error) {
+ this.body = error;
+ }
+ }
+}
+
module.exports = {
+ find: async (ctx) => {
+ ctx.body = await Post.find(this.params);
+ }
+}
+
The services files should stay as they are. Your generator functions can be converted into async functions but it shouldn't be necessary.
The models didn't change a lot. The autoCreatedAt
, autoUpdatedAt
and autoPK
attributes have been removed and replaced by the hasTimestamps
attribute.
The hasTimestamps
options will only work with Bookshelf. Also you need to create the created_at
and updated_at
columns manually.
The enum
type is not available for now. Also, the validations are not working properly. It means that most of the validations have to be done manually.
{
+ "identity": "pet",
+ "connection": "mongoDBServer",
+ "schema": true,
+ "attributes": {
+ "name": {
+ "type": "string",
+ "required": true
+ },
+ "gender": {
+ "type": "string",
+ "enum": ["male", "female"]
+ },
+ "age": {
+ "type": "int",
+ "max": 100
+ },
+ "birthDate": {
+ "type": "date"
+ },
+ "breed": {
+ "type": "string"
+ }
+ },
+ "autoPK": true,
+ "autoCreatedAt": true,
+ "autoUpdatedAt": true
+}
+
{
+ "connection": "mongoDBServer",
+ "options": {
+ "hasTimestamps": true
+ },
+ "attributes": {
+ "name": {
+ "type": "string",
+ "required": true
+ },
+ "gender": {
+ "type": "string"
+ },
+ "age": {
+ "type": "int",
+ "max": 100
+ },
+ "birthDate": {
+ "type": "date"
+ },
+ "breed": {
+ "type": "string"
+ }
+ }
+}
+
We were based on the popular Waterline ORM. As we said in our blog posts, Waterline suffers from a lack of maintenance and we decided to move to more specific ORMs depending on the database. It increases the performances and unblock particular features for the users. Currently, we are supporting these databases:
This major change means that you will have to rewrite every single query of your app. So, please to be sure that you need to switch to the new version of Strapi before starting the migration.
module.exports = {
+ find: function *() {
+ try {
+ this.body = yield Post.find(this.params);
+ } catch (error) {
+ this.body = error;
+ }
+ },
+
+ findOne: function *() {
+ try {
+ this.body = yield Post.findOne(this.params);
+ } catch (error) {
+ this.body = error;
+ }
+ },
+
+ // POST request
+ create: function *() {
+ try {
+ this.body = yield Post.create(this.request.body);
+ } catch (error) {
+ this.body = error;
+ }
+ },
+
+ // PUT request
+ update: function *() {
+ try {
+ this.body = yield Post.update(this.params.id, this.request.body);
+ } catch (error) {
+ this.body = error;
+ }
+ },
+
+ // DELETE request
+ delete: function *() {
+ try {
+ this.body = yield Post.destroy(this.params);
+ } catch (error) {
+ this.body = error;
+ }
+ }
+};
+
module.exports = {
+ find: async (ctx) => {
+ // Bookshelf
+ ctx.body = await Post.forge(this.params).fetchAll();
+ // Mongoose
+ ctx.body = await Post.find(this.params);
+ },
+
+ findOne: async (ctx) => {
+ // Bookshelf
+ ctx.body = await Post.forge(this.params).fetch();
+ // Mongoose
+ ctx.body = await Post.findOne(this.params);
+ },
+
+ create: async (ctx) => {
+ // Bookshelf
+ ctx.body = await Post.forge(this.request.body).save();
+ // Mongoose
+ ctx.body = await Post.create(this.request.body);
+ },
+
+ update: async (ctx) => {
+ // Bookshelf
+ ctx.body = await Post.forge({ id: 1234 }).save(this.request.body, {
+ patch: true
+ });
+ // Mongoose
+ ctx.body = await Post.update({ id: 1234 }, this.request.body);
+ },
+
+ delete: async (ctx) => {
+ // Bookshelf
+ ctx.body = await Post.forge({ id: 1234 }).destroy();
+ // Mongoose
+ ctx.body = await Post.findOneAndRemove({ id: 1234 });
+ }
+}
+
You will have more changes if your project is based on a SQL database. Waterline and Mongoose are almost sharing the same API.
We decided to reduce the core to the minimum. So, we removed middlewares with features that shouldn't be handled by a Node.js server such as:
strapi-plugin-graphql
very soon.strapi-views
hook.We are not able to give you a solution at the moment. As we said above, we will develop in the next weeks a dedicated plugin to integrate GraphQL into a project.
You should take a look at these articles to configure SSL and proxy with nginx:
It works exactly as before. You need to add strapi-views
into your app's dependencies and configure the views as below:
Path — ./config/environments/**/custom.json
{
+ "views": {
+ "enabled": true,
+ "map": {
+ "ejs": "ejs"
+ },
+ "viewExt": "ejs",
+ "debug": true,
+ "cache": true
+ }
+}
+
You might have to install the template engine by your own (ex: npm install ejs --save
).
Boom is deeply integrated into the core which allows you to enjoy the entire Boom API through the context of your request. Every error throw in your project will be intercepted and decorated with Boom.
module.exports = {
+ // GET request
+ find: function *() {
+ try {
+ const posts = yield Post.find(this.params);
+
+ if (posts.length === 0) {
+ ctx.status = 404;
+ ctx.body = 'There are no posts!';
+ } else {
+ ctx.body = posts;
+ }
+ } catch (error) {
+ this.body = error;
+ }
+ }
+};
+
module.exports = {
+ // GET request
+ find: async (ctx) => {
+ const posts = await Post.find(this.params);
+
+ if (post.length === 0) {
+ ctx.notFound('There are no posts!'); // Set status to 404 and decorates error into a Boom object.
+ } else {
+ ctx.send(posts); // Set status to 200.
+ }
+ }
+};
+
This section explains how the 'back-end part' of your plugin works.
The plugin API routes are defined in the ./plugins/**/config/routes.json
file.
Please refer to router documentation for informations.
Route prefix
Each route of a plugin is prefixed by the name of the plugin (eg: /my-plugin/my-plugin-route
).
To disable the prefix, add the prefix
attribute to each concerned route, like below:
{
+ "method": "GET",
+ "path": "/my-plugin-route",
+ "handler": "MyPlugin.action",
+ "prefix": false
+}
+
The CLI can be used to generate files in the plugins folders.
Please refer to the CLI documentation for more informations.
Controllers contain functions executed according to the requested route.
Please refer to the Controllers documentation for more informations.
A plugin can have its own models.
Sometimes it happens that the plugins inject models that have the same name as yours. Let's take a quick example.
You already have User
model defining in your ./api/user/models/User.settings.json
API. And you decide to install the Users & Permissions
plugin. This plugin also contains a User
model. To avoid the conflicts, the plugins' models are not globally exposed which means you cannot access to the plugin's model like this:
module.exports = {
+ findUser: async function (params) {
+ // This `User` global variable will always make a reference the User model defining in your `./api/xxx/models/User.settings.json`.
+ return await User.find();
+ }
+}
+
Also, the table/collection name won't be users
because you already have a User
model. That's why, the framework will automatically prefix the table/collection name for this model with the name of the plugin. Which means in our example, the table/collection name of the User
model of our plugin Users & Permissions
will be users-permissions_users
. If you want to force the table/collection name of the plugin's model, you can add the collectionName
attribute in your model.
Please refer to the Models documentation for more informations.
A plugin can also use a globally exposed policy in the current Strapi project.
{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/",
+ "handler": "MyPlugin.index",
+ "config": {
+ "policies": [
+ "global.isAuthenticated"
+ ]
+ }
+ }
+ ]
+}
+
A plugin can have its own policies, such as adding security rules. For instance, if the plugin includes a policy named isAuthenticated
, the syntax to use this policy would be:
{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/",
+ "handler": "MyPlugin.index",
+ "config": {
+ "policies": [
+ "plugins.myPlugin.isAuthenticated"
+ ]
+ }
+ }
+ ]
+}
+
Please refer to the Policies documentation for more informations.
Strapi supports multiple ORMs in order to let the users choose the database management system that suits their needs. Hence, each plugin must be compatible with at least one ORM. Each plugin contains a folder named queries
in ./plugins/**/api/queries
. A folder must be created for each ORM (eg. mongoose
) with a file named mongoose.js
which exports the Mongoose ORM related queries.
The queries are accessible through the strapi.query()
method, which automatically contains the queries according to the ORM used by the model.
Mongoose ORM queries definition:
Path — ./plugins/my-plugin/api/config/queries/mongoose/index.js
.
module.exports = {
+ getUsers: async (params) => {
+ return User.find(params);
+ }
+}
+
Bookshelf ORM queries definition:
Path — ./plugins/my-plugin/api/config/queries/bookshelf/index.js
.
module.exports = {
+ getUsers: async (params) => {
+ return User.fetchAll(params);
+ }
+}
+
Usage from the plugin:
Path — ./plugins/my-plugin/api/controllers/index.js
.
module.exports = {
+ getUsers: async () => {
+ // Get parameters from the request
+ const { limit, sort } = ctx.request.query;
+
+ // Get the list of users using the plugins queries
+ const users = await strapi.query('User').getUsers({ limit, sort });
+
+ // Send the list of users as response
+ ctx.body = users;
+ }
+}
+
Each function in the query file is bound with the ORM's model. It means that you can create generic query very easily. This feature is useful for CRUD such as we did in the Content Manager plugin.
Mongoose ORM generic queries:
Path — ./plugins/my-plugin/api/config/queries/mongoose/index.js
.
module.exports = {
+ getAll: async function (params) {
+ // this refers to the Mongoose model called in the query
+ // ex: strapi.query('User').getAll(), this will be equal to the User Mongoose model.
+ return this.find(params);
+ }
+}
+
Bookshelf ORM generic queries:
Path — ./plugins/my-plugin/api/config/queries/bookshelf/index.js
.
module.exports = {
+ getAll: async function (params) {
+ // this refers to the Bookshelf model called in the query
+ // ex: strapi.query('User').getAll(), this will be equal to the User Bookshelf model.
+ return this.fetchAll(params);
+ }
+}
+
Usage from the plugin:
Path — ./plugins/my-plugin/api/controllers/index.js
.
module.exports = {
+ getUsers: async () => {
+ // Get parameters from the request
+ const { limit, sort } = ctx.request.query;
+
+ // Get the list of users using the plugin's queries
+ const users = await strapi.query('User').getAll({ limit, sort });
+
+ // Send the list of users as response
+ ctx.body = users;
+ }
+}
+
This section explains how to create your plugin interface in the admin panel. Refer to the Plugin Development Quick Start Section to start the project in development mode.
Strapi's admin panel and plugins system aim to be an easy and powerful way to create new features.
The admin panel is a React application which can embed other React applications. These other React applications are the admin
parts of each Strapi's plugins.
The routing is based on the React Router V4, due to it's implementation each route is declared in the containers/App/index.js
file.
Also, we chose to use the Switch Router because it renders a route exclusively.
Route declaration :
Let's say that you want to create a route /user
with params /:id
associated with the container UserPage.
The declaration would be as followed :
Path — plugins/my-plugin/admin/src/containers/App/index.js
.
import React from 'react';
+import UserPage from 'containers/UserPage';
+
+// ...
+
+class App extends React.Component {
+ // ...
+
+ render() {
+ return (
+ <div className={styles.myPlugin}>
+ <Switch>
+ <Route exact path="/plugins/my-plugin/user/:id" component={UserPage} />
+ </Switch>
+ </div>
+ );
+ }
+}
+
+// ...
+
See the Front-end Use Cases for more informations.
Each plugin has its own data store, so it stays completely independent from the others.
Data flow is controlled thanks to Redux and redux-sagas.
The Bootstrap styles are inherited by the plugins. However, each component has its own styles, so it possible to completely customize it.
See the plugin styles for informations on its concept.
To style a plugin component:
styles.scss
file in the component directoryindex.js
file (import styles from './styles.scss';
)styles.scss
file.wrapper {
+ display: block;
+ background: red;
+ height: 100px;
+ width: 100px;
+}
+
Use this style in the component: <div className={styles.wrapper}></div>
.
if you want to use several classes:
import cn from 'classnames';
+import styles from './styles.scss';
+
+// ...
+
+return (
+ <div className={cn(styles.wrapper, styles.otherClass)}>{this.props.children}</div>
+);
+
+// ...
+
+
React Intl provides React components and an API to format dates, numbers, and strings, including pluralization and handling translations.
Usage
We recommend to set all your components text inside the translations folder.
The example below shows how to use i18n inside your plugin.
Define all your ids with the associated message:
Path — ./plugins/my-plugin/admin/src/translations/en.json
.
{
+ "notification.error.message": "An error occurred"
+}
+
Path — ./plugins/my-plugin/admin/src/translations/fr.json
{
+ "notification.error.message": "Une erreur est survenue"
+}
+
Usage inside a component
Path — ./plugins/my-plugin/admin/src/components/Foo/index.js
.
import { FormattedMessage } from 'react-intl';
+import SomeOtherComponent from 'components/SomeOtherComponent';
+
+const Foo = (props) => (
+ <div className={styles.foo}>
+ <FormattedMessage id="my-plugin.notification.error.message" />
+ <SomeOtherComponent {...props} />
+ </div>
+)
+
+export default Foo;
+
See the documentation for more extensive usage.
You can use generators to create React components or containers for your plugin.
cd plugins/my-plugin
npm run generate
and choose the type of component your want to createThis section gives use cases examples on front-end plugin development.
This section contains advanced resources to develop plugins.
The ExtendComponent
allows you to inject design from one plugin into another.
Let's say that you want to enable another plugin to inject a component into the top area of your plugin's container called FooPage
;
Path — ./plugins/my-plugin/admin/src/containers/FooPage/actions.js
.
import {
+ ON_TOGGLE_SHOW_LOREM,
+} from './constants';
+
+export function onToggleShowLorem() {
+ return {
+ type: ON_TOGGLE_SHOW_LOREM,
+ };
+}
+
Path — ./plugins/my-plugin/admin/src/containers/FooPage/index.js
.
import React from 'react';
+import { connect } from 'react-redux';
+import { bindActionCreators, compose } from 'redux';
+import { createStructuredSelector } from 'reselect';
+import PropTypes from 'prop-types';
+
+// Import the ExtendComponent
+import ExtendComponent from 'components/ExtendComponent';
+
+// Utils
+import injectReducer from 'utils/injectReducer';
+
+// Actions
+import { onToggleShowLorem } from './action'
+
+import reducer from './reducer';
+
+// Selectors
+import { makeSelectShowLorem } from './selectors';
+
+class FooPage extends React.Component {
+ render() {
+ const lorem = this.props.showLorem ? <p>Lorem ipsum dolor sit amet, consectetur adipiscing</p> : '';
+ return (
+ <div>
+ <h1>This is FooPage container</h1>
+ <ExtendComponent
+ area="top"
+ container="FooPage"
+ plugin="my-plugin"
+ {...props}
+ />
+ {lorem}
+ </div>
+ );
+ }
+}
+
+FooPage.propTypes = {
+ onToggleShowLorem: PropTypes.func.isRequired,
+ showLorem: PropTypes.bool.isRequired,
+};
+
+function mapDispatchToProps(dispatch) {
+ return bindActionCreators(
+ {
+ onToggleShowLorem,
+ },
+ dispatch,
+ );
+}
+
+const mapStateToProps = createStructuredSelector({
+ showLorem: makeSelectShowLorem(),
+});
+
+const withConnect = connect(mapDispatchToProps, mapDispatchToProps);
+const withReducer = injectReducer({ key: 'fooPage', reducer });
+
+export default compose(
+ withReducer,
+ withConnect,
+)(FooPage);
+
Path — ./plugins/my-plugin/admin/src/containers/FooPage/reducer.js
.
import { fromJS } from 'immutable';
+import { ON_TOGGLE_SHOW_LOREM } from './constants';
+
+const initialState = fromJS({
+ showLorem: false,
+});
+
+function fooPageReducer(state= initialState, action) {
+ switch (action.type) {
+ case ON_TOGGLE_SHOW_LOREM:
+ return state.set('showLorem', !state.get('showLorem'));
+ default:
+ return state;
+ }
+}
+
+export default fooPageReducer;
+
Path — ./plugins/my-plugin/admin/src/containers/FooPage/selectors.js
.
import { createSelector } from 'reselect';
+
+/**
+* Direct selector to the fooPage state domain
+*/
+
+const selectFooPageDomain = () => state => state.get('fooPage');
+
+/**
+* Other specific selectors
+*/
+
+const makeSelectShowLorem = () => createSelector(
+ selectFooPageDomain(),
+ (substate) => substate.get('showLorem'),
+);
+
+export { makeSelectShowLorem };
+
That's all now your plugin's container is injectable!
Let's see how to inject a React Component from a plugin into another.
Path - ./plugins/another-plugin/admin/src/extendables/BarContainer/index.js
;
import React from 'react';
+import PropTypes from 'prop-types';
+
+// Import our Button component
+import Button from 'components/Button';
+
+// Other imports such as actions, selectors, sagas, reducer...
+
+class BarContainer extends React.Component {
+ render() {
+ return (
+ <div>
+ <Button primary onClick={this.props.onToggleShowLorem}>
+ Click me to show lorem paragraph
+ </Button>
+ </div>
+ );
+ }
+}
+
+BarContainer.propTypes = {
+ onToggleShowLorem: PropTypes.func,
+};
+
+BarContainer.defaultProps = {
+ onToggleShowLorem: () => {},
+};
+
+export default BarContainer;
+
You have to create a file called injectedComponents.js
at the root of your another-plugin
src folder.
Path — ./plugins/another-plugin/admin/src/injectedComponents.js
.
import BarContainer from 'extendables/BarContainer';
+
+// export an array containing all the injected components
+export default [
+ {
+ area: 'top',
+ container: 'FooPage',
+ injectedComponent: BarContainer,
+ plugin: 'my-plugin',
+ },
+];
+
Just by doing so, the another-plugin
will add a Button
which toggles the lorem
paragraph in the FooPage
view.
If you have a container which can be a child of several other containers (i.e. it doesn't have a route); you'll have to inject it directly in the ./plugins/my-plugin/admin/src/containers/App/index.js
file as follows :
Path — ./plugins/my-plugin/admin/src/containers/App/index.js
.
// ...
+import fooReducer from 'containers/Foo/reducer';
+import fooSaga from 'container/Foo/sagas';
+
+import saga from './sagas';
+import { makeSelectFoo } from './selectors';
+
+// ...
+
+export class App extends React.Component {
+ render() {
+ return (
+ <div className={styles.app}>
+ <Switch>
+ {*/ List of all your routes here */}
+ </Switch>
+ </div>
+ );
+ }
+}
+
+// ...
+
+function mapDispatchToProps(dispatch) {
+ return bindActionCreators(
+ {
+ },
+ dispatch
+ );
+}
+
+const mapStateToProps = createStructuredSelector({
+ // ...
+});
+
+const withConnect = connect(mapStateToProps, mapDispatchToProps);
+// Foo reducer
+const withFooReducer = injectReducer({ key: 'foo', reducer: fooReducer });
+// Global reducer
+const withReducer = injectReducer({ key: 'global', reducer });
+// Foo saga
+const withFooSaga = injectSaga({ key: 'foo', saga: fooSaga });
+// Global saga
+const withSaga = injectSaga({ key: 'global', saga });
+
+export default compose(
+ withFooReducer,
+ withReducer,
+ withFooSaga,
+ withSaga,
+ withConnect,
+)(App);
+
You can execute a business logic before your plugin is being mounted.
To do this, you need to create bootstrap.js
file at the root of your src
plugin's folder.
+This file must contains a default functions that returns a Promise
.
In this example, we want to populate the left menu with links that will refer to our Content Types.
Path — ./app/plugins/content-manager/admin/src/bootstrap.js
.
import { generateMenu } from 'containers/App/sagas';
+
+// This method is executed before the load of the plugin
+const bootstrap = (plugin) => new Promise((resolve, reject) => {
+ generateMenu()
+ .then(menu => {
+ plugin.leftMenuSections = menu;
+
+ resolve(plugin);
+ })
+ .catch(e => reject(e));
+});
+
+export default bootstrap;
+
You can prevent your plugin from being rendered if some conditions aren't met.
To disable your plugin's rendering, you can simply create requirements.js
file at the root of your src
plugin's folder.
+This file must contain a default function that returns a Promise
.
Let's say that you want to disable your plugin if the server autoReload config is disabled in development mode.
Path — ./app/config/environments/development/server.json
.
{
+ "host": "localhost",
+ "port": 1337,
+ "autoReload": {
+ "enabled": true
+ },
+ "cron": {
+ "enabled": false
+ }
+}
+
You'll first create a request to check if the autoReload
config is enabled.
Path — ./app/plugins/my-plugin/config/routes.json
.
{
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/autoReload",
+ "handler": "MyPlugin.autoReload",
+ "config": {
+ "policies": []
+ }
+ }
+ ]
+}
+
Then the associated handler:
Path — ./app/plugins/my-plugin/controllers/MyPlugin.js
.
const _ = require('lodash');
+const send = require('koa-send');
+
+module.exports = {
+ autoReload: async ctx => {
+ ctx.send({ autoReload: _.get(strapi.config.environments, 'development.server.autoReload', false) });
+ }
+}
+
Finally, you'll create a file called requirements.js
at the root of your plugin's src folder.
The default function exported must return a Promise
.
+If you wan't to prevent the plugin from being rendered you'll have to set plugin.preventComponentRendering = true;
.
+In this case, you'll have to set:
plugin.blockerComponentProps = {
+ blockerComponentTitle: 'my-plugin.blocker.title',
+ blockerComponentDescription: 'my-plugin.blocker.description',
+ blockerComponentIcon: 'fa-refresh',
+};
+
To follow the example above:
Path — ./app/plugins/my-plugin/admin/src/requirements.js
.
// Use our request helper
+import request from 'utils/request';
+
+const shouldRenderCompo = (plugin) => new Promise((resolve, request) => {
+ request('/my-plugin/autoReload')
+ .then(response => {
+ // If autoReload is enabled the response is `{ autoReload: true }`
+ plugin.preventComponentRendering = !response.autoReload;
+ // Set the BlockerComponent props
+ plugin.blockerComponentProps = {
+ blockerComponentTitle: 'my-plugin.blocker.title',
+ blockerComponentDescription: 'my-plugin.blocker.description',
+ blockerComponentIcon: 'fa-refresh',
+ blockerComponentContent: 'renderIde', // renderIde will add an ide section that shows the development environment server.json config
+ };
+
+ return resolve(plugin);
+ })
+ .catch(err => reject(err));
+});
+
+export default shouldRenderCompo;
+
You can render your own custom blocker by doing as follows:
Path — ./app/plugins/my-plugin/admin/src/requirements.js
.
// Use our request helper
+import request from 'utils/request';
+
+// Your custom blockerComponentProps
+import MyCustomBlockerComponent from 'components/MyCustomBlockerComponent';
+
+const shouldRenderCompo = (plugin) => new Promise((resolve, request) => {
+ request('/my-plugin/autoReload')
+ .then(response => {
+ // If autoReload is enabled the response is `{ autoReload: true }`
+ plugin.preventComponentRendering = !response.autoReload;
+
+ // Tell which component to be rendered instead
+ plugin.blockerComponent = MyCustomBlockerComponent;
+
+ return resolve(plugin);
+ })
+ .catch(err => reject(err));
+});
+
+export default shouldRenderCompo;
+
If your application is going to interact with some back-end application for data, we recommend using redux saga for side effect management. +This short tutorial will show how to fetch data using actions/reducer/sagas.
Path — ./plugins/my-plugin/admin/src/containers/FooPage/constants.js
export const DATA_FETCH = 'MyPlugin/FooPage/DATA_FETCH';
+export const DATA_FETCH_ERROR = 'MyPlugin/FooPage/DATA_FETCH_ERROR';
+export const DATA_FETCH_SUCCEEDED = 'MyPlugin/FooPage/DATA_FETCH_SUCCEEDED';
+
Path — ./plugins/my-plugin/admin/src/containers/FooPage/actions.js
import {
+ DATA_FETCH,
+ DATA_FETCH_ERROR,
+ DATA_FETCH_SUCCEEDED,
+} from './constants';
+
+export function dataFetch(params) {
+ return {
+ type: DATA_FETCH,
+ params,
+ };
+}
+
+export function dataFetchError(errorMessage) {
+ return {
+ type: DATA_FETCH_ERROR,
+ errorMessage,
+ };
+}
+
+export function dataFetchSucceeded(data) {
+ return {
+ type: DATA_FETCH_SUCCEEDED,
+ data,
+ };
+}
+
We strongly recommend to use Immutable.js to structure your data.
Path — ./plugins/my-plugin/admin/src/containers/FooPage/reducer.js
import { fromJS, Map } from 'immutable';
+import {
+ DATA_FETCH_ERROR,
+ DATA_FETCH_SUCCEEDED,
+} from './constants';
+
+const initialState = fromJS({
+ data: Map({}),
+ error: false,
+ errorMessage: '',
+ loading: true,
+});
+
+function fooPageReducer(state = initialState, action) {
+ switch (action.type) {
+ case DATA_FETCH_ERROR:
+ return state
+ .set('error', true)
+ .set('errorMessage', action.errorMessage)
+ .set('loading', false);
+ case DATA_FETCH_SUCCEEDED:
+ return state
+ .set('data', Map(action.data))
+ .set('error', false)
+ .set('errorMessage', '')
+ .set('loading', false);
+ default:
+ return state;
+ }
+}
+
+export default fooPageReducer;
+
Path — ./plugins/my-plugin/admin/src/containers/FooPage/sagas.js
import { LOCATION_CHANGE } from 'react-router-redux';
+import { takeLatest, put, fork, call, take, cancel } from 'redux-saga/effects';
+
+// Use our request helper
+import request from 'utils/request';
+
+// Actions
+import { dataFetchError, dataFetchSucceeded } from './actions';
+import { DATA_FETCH } from './constants';
+
+export function* fetchData(action) {
+ try {
+ const requestUrl = `/baseUrl/${action.params}`;
+ const opts = {
+ method: 'GET',
+ };
+
+ // Fetch data
+ const response = yield call(request, requestUrl, opts);
+
+ // Pass the response to the reducer
+ yield put(dataFetchSucceeded(response));
+
+ } catch(error) {
+ yield put(dataFetchError(error));
+ }
+}
+
+// Individual export for testing
+function* defaultSaga() {
+ // Listen to DATA_FETCH event
+ const fetchDataWatcher = yield fork(takeLatest, DATA_FETCH, fetchData);
+
+ // Cancel watcher
+ yield take(LOCATION_CHANGE);
+
+ yield cancel(fetchDataWatcher);
+}
+
+export default defaultSaga;
+
N.B. You can use a selector in your sagas :
import { put, select, fork, call, take, cancel } from 'redux-saga/effects';
+import { makeSelectUserName } from './selectors';
+
+export function* foo() {
+ try {
+ const userName = yield select(makeSelectUserName());
+
+ // ...
+ } catch(error) {
+ // ...
+ }
+}
+
+function defaultSaga() {
+ // ...
+}
+
+export default defaultSaga;
+
Reselect is a library used for slicing your redux state and providing only the relevant sub-tree to a react component. It has three key features:
Creating a selector:
Path — ./plugins/my-plugin/admin/src/containers/FooPage/selectors.js
import { createSelector } from 'reselect';
+
+/**
+* Direct selector to the fooPage state domain
+*/
+const selectFooPageDomain = () => state => state.get('fooPage');
+
+/**
+ * Other specific selectors
+ */
+
+ const makeSelectLoading = () => createSelector(
+ selectFooPageDomain(),
+ (substate) => substate.get('loading'),
+ );
+
+/**
+ * Default selector used by FooPage
+ */
+
+const selectFooPage = () => createSelector(
+ selectFooDomain(),
+ (substate) => substate.toJS()
+);
+
+export default selectFooPage;
+export { makeSelectLoading };
+
+
Path — ./plugins/my-plugin/admin/src/containers/FooPage/index.js
import React from 'react';
+import { bindActionCreators } from 'redux';
+import { connect, compose } from 'react-redux';
+import PropTypes from 'prop-types';
+
+// Main router
+import { router } from 'app';
+
+// Utils
+import injectSaga from 'utils/injectSaga';
+import injectReducer from 'utils/injectReducer';
+
+// Actions
+import { dataFetch } from './actions';
+// sagas
+import saga from './sagas';
+// Selectors
+import selectFooPage from './selectors';
+// Reducer
+import reducer from './reducer';
+
+export class FooPage extends React.Component {
+ componentWillReceiveProps(nextProps) {
+ if (this.props.error !== nextProps.error && nextProps.error) {
+ strapi.notification.error(nextProps.errorMessage);
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.match.pathname !== this.props.pathname) {
+ this.props.dataFetch(this.props.match.params.bar);
+ }
+ }
+
+ render() {
+ if (this.props.error) return <div>An error occurred</div>;
+
+ return (
+ <div>
+ <h4>Data display</h4>
+ <span>{this.props.data.foo}</span>
+ <span>{this.props.data.bar}</span>
+ </div>
+ );
+ }
+
+ FooPage.propTypes = {
+ data: PropTypes.object.isRequired,
+ dataFetch: PropTypes.func.isRequired,
+ error: PropTypes.bool.isRequired,
+ errorMessage: PropTypes.string.isRequired,
+ match: PropTypes.object.isRequired,
+ };
+
+ const mapStateToProps = selectFoo();
+
+ function mapDispatchToProps(dispatch) {
+ return bindActionCreators(
+ {
+ dataFetch,
+ },
+ dispatch
+ );
+ }
+
+ const withConnect = connect(mapStateToProps, mapDispatchToProps);
+ const withReducer = injectReducer({ key: 'fooPage', reducer });
+ const withSagas = injectSaga({ key: 'fooPage', saga });
+
+ export default compose(
+ withReducer,
+ withSagas,
+ withConnect,
+ )(FooPage);
+}
+
The logic of a plugin is located at his root directory ./plugins/**
. The admin panel related parts of each plugin is contained in the /admin
folder.
+The folders and files structure is the following:
/plugin
+└─── admin // Contains the plugin's front-end
+| └─── build // Webpack build of the plugin
+| └─── src // Source code directory
+| └─── bootstrap.js // (Optional) Contains the logic to execute before rendering the plugin
+| └─── components // Contains the list of React components used by the plugin
+| └─── containers
+| | └─── App // Container used by every others containers
+| | └─── HomePage
+| | └─── action.js // List of Redux actions used by the current container
+| | └─── constants.js // List of actions constants
+| | └─── index.js // React component of the current container
+| | └─── reducer.js // Redux reducer used by the current container
+| | └─── sagas.js // List of sagas functions
+| | └─── selectors.js // List of selectors
+| | └─── styles.scss // Style of the current container
+| |
+| └─── requirements.js // (Optional) Contains the logic to prevent a plugin from being rendered
+| └─── translations // Contains the translations to make the plugin internationalized
+| └─── en.json
+| └─── fr.json
+└─── config // Contains the configurations of the plugin
+| └─── functions
+| | └─── bootstrap.js // Asynchronous bootstrap function that runs before the app gets started
+| └─── policies // Folder containing the plugin's policies
+| └─── queries // Folder containing the plugin's models queries
+| └─── routes.json // Contains the plugin's API routes
+└─── controllers // Contains the plugin's API controllers
+└─── middlewares // Contains the plugin's middlewares
+└─── models // Contains the plugin's API models
+└─── services // Contains the plugin's API services
+
// ...
+
+import PluginLeftMenu from 'components/PluginLeftMenu';
+
+// ...
+
+const Foo = (props) => {
+ const sections = [
+ {
+ name: 'section 1',
+ items: [
+ { icon: 'fa-caret-square-o-right', name: 'link 1'},
+ { icon: 'fa-caret-square-o-right', name: 'link 2'},
+ ],
+ },
+ ];
+
+ return (
+ <div className={styles.foo}>
+ <div className="container-fluid">
+ <div className="row">
+ <PluginLeftMenu
+ sections={sections}
+ />
+ </div>
+ </div>
+ </div>
+ );
+}
+
+export default Foo;
+
+// ...
+
Property | Type | Required | Description |
---|---|---|---|
addCustomSection | function | no | Allows to add another section after the initial one. |
basePath | string | yes | For example the basePath of the route '/plugins/my-plugin/foo/bar' is 'my-plugin/categories' |
renderCustomLink | function | no | Allows to override the design and the behavior of a link |
sections | array | yes | Sections of the component menu |
// ...
+
+import PluginLeftMenu from 'components/PluginLeftMenu';
+
+// ...
+
+const addCustomSection = (sectionStyles) => (
+ // You have access to the section styles
+ <div className={sectionStyles.pluginLeftMenuSection}>
+ <p>
+ DOCUMENTATION
+ </p>
+ <ul>
+ <li>
+ Read more about strapi in our <a href="http://strapi.io/documentation" target="_blank">documentation</a>
+ </li>
+ </ul>
+ </div>
+)
+
+const renderAddLink = (props, customLinkStyles) => (
+ <li className={customLinkStyles.pluginLeftMenuLink}>
+ <div className={`${customLinkStyles.liInnerContainer}`} onClick={this.handleAddLinkClick}>
+ <div>
+ <i className={`fa ${props.link.icon}`} />
+ </div>
+ <span>{props.link.name}</span>
+ </div>
+ </li>
+)
+
+const renderCustomLink = (props, linkStyles) => {
+ if (props.link.name === 'bar') return this.renderAddLink(props, linkStyles);
+
+ return (
+ <li className={linkStyles.pluginLeftMenuLink}>
+ <NavLink className={linkStyles.link} to={`/plugins/my-plugin/foo/${props.link.name}`} activeClassName={linkStyles.linkActive}>
+ <div>
+ <i className={`fa fa-caret-square-o-right`} />
+ </div>
+ <div className={styles.contentContainer}>
+ <span className={spanStyle}>{props.link.name}</span>
+ </div>
+
+ </NavLink>
+ </li>
+ );
+}
+
+const Foo = (props) => {
+ const sections = [
+ {
+ name: 'section 1',
+ items: [
+ { icon: 'fa-caret-square-o-right', name: 'link 1'},
+ { icon: 'fa-caret-square-o-right', name: 'link 2'},
+ ],
+ },
+ ];
+
+ return (
+ <div className={styles.foo}>
+ <div className="container-fluid">
+ <div className="row">
+ <PluginLeftMenu
+ addCustomSection={addCustomSection}
+ sections={sections}
+ renderCustomLink={renderCustomLink}
+ basePath="my-plugins/foo"
+ />
+ </div>
+ </div>
+ </div>
+ );
+}
+
+// ...
+
+export default Foo;
+
To facilitate the development of a plugin, we drastically reduce the amount of commands necessary to install the entire development environment. Before getting started, you need to have Node.js (v8) and npm (v5) installed.
To setup the development environment please follow the instructions below:
git clone git@github.com:strapi/strapi.git
.npm run setup
at the root of the directory.You can run npm run setup:build
to build the plugins' admin (the setup time will be longer)
If the installation failed, please remove the global packages related to Strapi. The command npm ls strapi
will help you to find where your packages are installed globally.
Create a development project
cd /path/to/my/folder
.strapi new myDevelopmentProject --dev
.To generate a new plugin run the following commands:
cd myDevelopmentProject && strapi generate:plugin my-plugin
.strapi-helper-plugin
dependency in your project folder cd pathToMyProject/myDevelopmentProject/plugins/my-plugin && npm link strapi-helper-plugin
.strapi-helper-plugin
dependency in the analytics
plugin folder cd pathToMyProject/myDevelopmentProject/plugins/analytics && npm link strapi-helper-plugin
.cd pathToMyProject/myDevelopmentProject/admin && npm start
and go to the following url http://localhost:4000/admin.strapi start
.Your are now ready to develop your own plugin and live-test your updates!
Strapi provides helpers so you don't have to develop again and again the same generic functions.
auth.js
lets you get, set and delete data in either the browser's localStorage
or sessionStorage
.
Name | Description |
---|---|
clear(key) | Remove the data in either localStorage or sessionStorage |
clearAppStorage() | Remove all data from both storage |
clearToken() | Remove the user's jwt Token in the appropriate browser's storage |
clearUserInfo() | Remove the user's info from storage |
get(key) | Get the item in the browser's storage |
getToken() | Get the user's jwtToken |
getUserInfo() | Get the user's infos |
set(value, key, isLocalStorage) | Set an item in the sessionStorage . If true is passed as the 3rd parameter it sets the value in the localStorage |
setToken(value, isLocalStorage) | Set the user's jwtToken in the sessionStorage . If true is passed as the 2nd parameter it sets the value in the localStorage |
setUserInfo(value, isLocalStorage) | Set the user's info in the sessionStorage . If true is passed as the 2nd parameter it sets the value in the localStorage |
import auth from 'utils/auth';
+
+// ...
+//
+auth.setToken('12345', true); // This will set 1234 in the browser's localStorage associated with the key: jwtToken
+
This function allows to darken a color.
import { darken } from 'utils/colors';
+
+const linkColor = darken('#f5f5f5', 1.5); // Will darken #F5F5F5 by 1.5% which gives #f2f2f2.
+
The helpers allows to retrieve the query parameters in the URL.
import getQueryParameters from 'utils/getQueryParameters';
+
+const URL = '/create?source=users-permissions';
+const source = getQueryParameters(URL, 'source');
+
+console.log(source); // users-permissions
+
+
A request helper is available to handle all requests inside a plugin.
It takes three arguments:
requestUrl
: The url we want to fetch.options
: Please refer to this documentation.true
: This third argument is optional. If true is passed the response will be sent only if the server has restarted check out the example.Path - /plugins/my-plugin/admin/src/containers/**/sagas.js
.
import { call, fork, put, takeLatest } from 'redux-saga/effects';
+
+// Our request helper
+import request from 'utils/request';
+import { dataFetchSucceeded, dataFetchError } from './actions';
+import { DATA_FETCH } from './constants';
+
+export function* fetchData(action) {
+ try {
+ const opts = {
+ method: 'GET',
+ };
+ const requestUrl = `/my-plugin/${action.endPoint}`;
+ const data = yield call(request, requestUrl, opts);
+
+ yield put(dataFetchSucceeded(data));
+ } catch(error) {
+ yield put(dataFetchError(error))
+ }
+}
+
+// Individual exports for testing
+function* defaultSaga() {
+ yield fork(takeLatest, DATA_FETCH, fetchData);
+}
+
+export default defaultSaga;
+
Let's say that we have a container that fetches Content Type configurations depending on URL change.
Here we want to create a route /content-type/:contentTypeName
for the ContentTypePage
container.
Path — ./plugins/my-plugin/admin/src/container/App/index.js
.
import React from 'react';
+import { connect } from 'react-redux';
+import { bindActionCreators, compose } from 'redux';
+
+import { createStructuredSelector } from 'reselect';
+import { Switch, Route, withRouter } from 'react-router-dom';
+import PropTypes from 'prop-types';
+import { pluginId } from 'app';
+
+import ContentTypePage from 'containers/ContentTypePage';
+import styles from './styles.scss';
+
+class App extends React.Component {
+ render() {
+ return (
+ <div className={`${pluginId} ${styles.app}`}>
+ <Switch>
+ <Route exact path="/plugins/my-plugin/content-type/:contentTypeName" component={ContentTypePage} />
+ </Switch>
+ </div>
+ );
+ }
+}
+
+App.contextTypes = {
+ router: PropTypes.object.isRequired,
+};
+
+export function mapDispatchToProps(dispatch) {
+ return bindActionCreators(
+ {},
+ dispatch
+ );
+}
+
+const mapStateToProps = createStructuredSelector({});
+const withConnect = connect(mapStateToProps, mapDispatchToProps);
+
+export default compose(
+ withConnect,
+)(App);
+
+
Let's declare the needed constants to handle fetching data:
Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/constants.js
.
export const DATA_FETCH = 'myPlugin/ContentTypePage/DATA_FETCH';
+export const DATA_FETCH_ERROR = 'myPlugin/ContentTypePage/DATA_FETCH_ERROR';
+export const DATA_FETCH_SUCCEEDED = 'myPlugin/ContentTypePage/DATA_FETCH_SUCCEEDED';
+
Let's declare our actions.
Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/actions.js
.
import {
+ DATA_FETCH,
+ DATA_FETCH_ERROR,
+ DATA_FETCH_SUCCEEDED,
+} from './constants';
+
+export function dataFetch(contentTypeName) {
+ return {
+ type: DATA_FETCH,
+ contentTypeName,
+ };
+}
+
+export function dataFetchError(errorMessage) {
+ return {
+ type: DATA_FETCH_ERROR,
+ errorMessage,
+ };
+}
+
+export function dataFetchSucceeded(data) {
+ // data will look like { data: { name: 'User', description: 'Some description' } }
+ return {
+ type: DATA_FETCH_SUCCEEDED,
+ data,
+ };
+}
+
Please refer to the Immutable documentation for informations about data structure.
Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/reducer.js
.
import { fromJS, Map } from 'immutable';
+import {
+ DATA_FETCH,
+ DATA_FETCH_ERROR,
+ DATA_FETCH_SUCCEEDED
+} from './constants';
+
+const initialState = fromJS({
+ contentTypeName,
+ error: false,
+ errorMessage: '',
+ data: Map({}),
+});
+
+function contentTypePageReducer(state = initialState, action) {
+ switch (action.type) {
+ case DATA_FETCH:
+ return state.set('contentTypeName', action.contentTypeName);
+ case DATA_FETCH_ERROR:
+ return state
+ .set('error', true)
+ .set('errorMessage', action.errorMessage);
+ case DATA_FETCH_SUCCEEDED:
+ return state
+ .set('error', false)
+ .set('data', Map(action.data.data));
+ default:
+ return state;
+ }
+}
+
+export default contentTypePageReducer;
+
Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/selectors.js
.
import { createSelector } from 'reselect';
+
+/**
+ * Direct selector to the contentTypePage state domain
+ */
+const selectContentTypePageDomain = () => state => state.get('contentTypePage');
+
+/**
+ * Other specific selectors
+ */
+
+
+/**
+ * Default selector used by ContentTypePage
+ */
+
+const selectContentTypePage = () => createSelector(
+ selectContentTypePageDomain(),
+ (substate) => substate.toJS()
+);
+
+const makeSelectContentTypeName = () => createSelector(
+ selectContentTypePageDomain(),
+ (substate) => substate.get('contentTypeName');
+)
+export default selectContentTypePage;
+export { makeSelectContentTypeName, selectContentTypePageDomain };
+
Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/index.js
.
import React from 'react';
+import { connect } from 'react-redux';
+import { createStructuredSelector } from 'reselect';
+import { bindActionCreators, compose } from 'redux';
+import { NavLink } from 'react-router-dom';
+import PropTypes from 'prop-types';
+import { map } from 'lodash';
+
+// Utils to create the container's store
+import injectSaga from 'utils/injectSaga';
+import injectReducer from 'utils/injectReducer';
+
+import { dataFetch } from './actions';
+import { selectContentTypePage } from './selectors';
+import saga from './sagas';
+import reducer from './reducer';
+import styles from './styles.scss';
+
+export class ContentTypePage extends React.Component { // eslint-disable-line react/prefer-stateless-function
+ constructor(props) {
+ super(props);
+
+ this.links = [
+ {
+ to: 'plugin/my-plugin/content-type/product',
+ info: 'Product',
+ },
+ {
+ to: 'plugin/my-plugin/content-type/user',
+ info: 'User',
+ },
+ ];
+ }
+
+ componentDidMount() {
+ this.props.dataFetch(this.props.match.params.contentTypeName);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.match.params.contentTypeName !== this.props.match.params.contentTypeName) {
+ this.props.dataFetch(nextProps.match.params.contentTypeName);
+ }
+ }
+
+ render() {
+ return (
+ <div className={styles.contentTypePage}>
+ <div>
+ <ul>
+ {map(this.links, (link, key) => (
+ <li key={key}>
+ <NavLink to={link.to}>{link.info}</NavLink>
+ </li>
+ ))}
+ </ul>
+ </div>
+ <div>
+ <h1>{this.props.data.name}</h1>
+ <p>{this.props.data.description}</p>
+ </div>
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = selectContentTypePage();
+
+function mapDispatchToProps(dispatch) {
+ return bindActionCreators(
+ {
+ dataFetch,
+ },
+ dispatch,
+ );
+}
+
+ContentTypePage.propTypes = {
+ data: PropTypes.object.isRequired,
+ dataFetch: PropTypes.func.isRequired,
+ match: PropTypes.object.isRequired,
+};
+
+const withConnect = connect(mapStateToProps, mapDispatchToProps);
+const withSaga = injectSaga({ key: 'contentTypePage', saga });
+const withReducer = injectReducer({ key: 'contentTypePage', reducer });
+
+export default compose(
+ withReducer,
+ withSaga,
+ withConnect,
+)(ContentTypePage);
+
The sagas.js
file is in charge of fetching data.
Path — ./plugins/my-plugin/admin/src/containers/ContentTypePage/sagas.js
.
import { LOCATION_CHANGE } from 'react-router-redux';
+import { takeLatest, call, take, put, fork, cancel, select } from 'redux-saga/effects';
+import request from 'utils/request';
+import {
+ dataFetchError,
+ dataFetchSucceeded,
+} from './actions';
+import { DATA_FETCH } from './constants';
+import { makeSelectContentTypeName } from './selectors';
+
+export function* fetchData() {
+ try {
+ const opts = { method: 'GET' };
+
+ // To make a POST request { method: 'POST', body: {Object} }
+
+ const endPoint = yield select(makeSelectContentTypeName());
+ const requestUrl = `my-plugin/**/${endPoint}`;
+
+ // Fetching data with our request helper
+ const data = yield call(request, requestUrl, opts);
+ yield put(dataFetchSucceeded(data));
+ } catch(error) {
+ yield put(dataFetchError(error.message));
+ }
+}
+
+function* defaultSaga() {
+ const loadDataWatcher = yield fork(takeLatest, DATA_FETCH, fetchData);
+
+ yield take(LOCATION_CHANGE);
+ yield cancel(loadDataWatcher);
+}
+
+export default defaultSaga;
+
Let's say that you want to develop a plugin that needs server restart on file change (like the settings-manager plugin) and you want to be aware of that to display some stuff..., you just have to send a third argument: true
to our request helper and it will ping a dedicated route and send the response when the server has restarted.
Path — ./plugins/my-plugin/admin/src/containers/**/sagas.js
.
import { takeLatest, call, take, put, fork, cancel, select } from 'redux-saga/effects';
+import request from 'utils/request';
+import {
+ submitSucceeded,
+ submitError,
+} from './actions';
+import { SUBMIT } from './constants';
+// Other useful imports like selectors...
+// ...
+
+export function* postData() {
+ try {
+ const body = { data: 'someData' };
+ const opts = { method: 'POST', body };
+ const requestUrl = `**yourUrl**`;
+
+ const response = yield call(request, requestUrl, opts, true);
+
+ if (response.ok) {
+ yield put(submitSucceeded());
+ } else {
+ yield put(submitError('An error occurred'));
+ }
+ } catch(error) {
+ yield put(submitError(error.message));
+ }
+}
+
+function* defaultSaga() {
+ yield fork(takeLatest, SUBMIT, postData);
+ // ...
+}
+
+export default defaultSaga;
+
Go further with Strapi, official and community tutorials are here to help you:
Hello John
'")]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),a("span",{attrs:{class:"token comment"}},[t._v("// HTML version of the email content.")]),t._v("\n text"),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v("'Hello John'")]),t._v(" "),a("span",{attrs:{class:"token comment"}},[t._v("// Text version of the email content.")]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),a("span",{attrs:{class:"token function"}},[t._v("then")]),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),a("span",{attrs:{class:"token keyword"}},[t._v("function")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),t._v("data"),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n console"),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),a("span",{attrs:{class:"token function"}},[t._v("log")]),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),t._v("data"),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),a("span",{attrs:{class:"token keyword"}},[t._v("catch")]),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),a("span",{attrs:{class:"token keyword"}},[t._v("function")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),t._v("err"),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n console"),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),a("span",{attrs:{class:"token function"}},[t._v("log")]),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),t._v("err"),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n")])])])},function(){var t=this,s=t.$createElement,a=t._self._c||s;return a("div",{staticClass:"language-js extra-class"},[a("pre",{pre:!0,attrs:{class:"language-js"}},[a("code",[t._v("strapi"),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),t._v("api"),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),t._v("email"),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),t._v("services"),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),t._v("email"),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),a("span",{attrs:{class:"token function"}},[t._v("send")]),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),a("span",{attrs:{class:"token keyword"}},[t._v("from")]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v("'contact@company.com'")]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),a("span",{attrs:{class:"token comment"}},[t._v("// Sender (defaults to `strapi.config.smtp.from`).")]),t._v("\n to"),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("[")]),a("span",{attrs:{class:"token string"}},[t._v("'john@doe.com'")]),a("span",{attrs:{class:"token punctuation"}},[t._v("]")]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),a("span",{attrs:{class:"token comment"}},[t._v("// Recipients list.")]),t._v("\n html"),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v("'Hello John
'")]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),a("span",{attrs:{class:"token comment"}},[t._v("// HTML version of the email content.")]),t._v("\n text"),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v("'Hello John'")]),t._v(" "),a("span",{attrs:{class:"token comment"}},[t._v("// Text version of the email content.")]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),a("span",{attrs:{class:"token keyword"}},[t._v("function")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),t._v("err"),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" data"),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),a("span",{attrs:{class:"token keyword"}},[t._v("if")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),t._v("err"),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n console"),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),a("span",{attrs:{class:"token function"}},[t._v("log")]),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),t._v("err"),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),t._v(" "),a("span",{attrs:{class:"token keyword"}},[t._v("else")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n console"),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),a("span",{attrs:{class:"token function"}},[t._v("log")]),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),t._v("data"),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n")])])])},function(){var t=this.$createElement,s=this._self._c||t;return s("h2",{attrs:{id:"email-api"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#email-api","aria-hidden":"true"}},[this._v("#")]),this._v(" Email API")])},function(){var t=this.$createElement,s=this._self._c||t;return s("div",{staticClass:"language-bash extra-class"},[s("pre",{pre:!0,attrs:{class:"language-bash"}},[s("code",[this._v("POST /email\n")])])])},function(){var t=this,s=t.$createElement,a=t._self._c||s;return a("div",{staticClass:"language-js extra-class"},[a("pre",{pre:!0,attrs:{class:"language-js"}},[a("code",[a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),a("span",{attrs:{class:"token keyword"}},[t._v("from")]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v("'contact@company.com'")]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),a("span",{attrs:{class:"token comment"}},[t._v("// Optional : sender (defaults to `strapi.config.smtp.from`).")]),t._v("\n to"),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("[")]),a("span",{attrs:{class:"token string"}},[t._v("'john@doe.com'")]),a("span",{attrs:{class:"token punctuation"}},[t._v("]")]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),a("span",{attrs:{class:"token comment"}},[t._v("// Recipients list.")]),t._v("\n html"),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v("'Hello John
'")]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),a("span",{attrs:{class:"token comment"}},[t._v("// HTML version of the email content.")]),t._v("\n text"),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v("'Hello John'")]),t._v(" "),a("span",{attrs:{class:"token comment"}},[t._v("// Text version of the email content.")]),t._v("\n"),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])])},function(){var t=this,s=t.$createElement,a=t._self._c||s;return a("div",{staticClass:"language-js extra-class"},[a("pre",{pre:!0,attrs:{class:"language-js"}},[a("code",[a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"sent"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token boolean"}},[t._v("true")]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"from"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"contact@company.com"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"to"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"john@doe.com"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"html"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"Hello John
"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"text"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"Hello John"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"template"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"default"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"lang"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"en"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"createdAt"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"2015-10-21T09:10:36.486Z"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"updatedAt"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"2015-10-21T09:10:36.871Z"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"id"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token number"}},[t._v("2")]),t._v("\n"),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])])},function(){var t=this.$createElement,s=this._self._c||t;return s("h2",{attrs:{id:"email-model"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#email-model","aria-hidden":"true"}},[this._v("#")]),this._v(" Email model")])}],!1,null,null,null);e.options.__file="email.md";s.default=e.exports}}]); \ No newline at end of file diff --git a/docs/.vuepress/dist/assets/js/12.124227d1.js b/docs/.vuepress/dist/assets/js/12.124227d1.js new file mode 100644 index 0000000000..b2d49d4fbe --- /dev/null +++ b/docs/.vuepress/dist/assets/js/12.124227d1.js @@ -0,0 +1 @@ +(window.webpackJsonp=window.webpackJsonp||[]).push([[12],{227:function(t,s,a){"use strict";a.r(s);var n=a(0),e=Object(n.a)({},function(){this.$createElement;this._self._c;return this._m(0)},[function(){var t=this,s=t.$createElement,a=t._self._c||s;return a("div",{staticClass:"content"},[a("h1",{attrs:{id:"graphql"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#graphql","aria-hidden":"true"}},[t._v("#")]),t._v(" GraphQL")]),t._v(" "),a("p",[t._v("GraphQL is a data querying language that allows you to execute complex nested\nrequests between your clients and server applications.")]),t._v(" "),a("h2",{attrs:{id:"configuration"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#configuration","aria-hidden":"true"}},[t._v("#")]),t._v(" Configuration")]),t._v(" "),a("p",[t._v("By default, GraphQL is enabled and the HTTP endpoint is "),a("code",[t._v("/graphql")]),t._v(".\nYou can override this settings in the "),a("code",[t._v("./config/general.json")]),t._v(" file.")]),t._v(" "),a("div",{staticClass:"language-js extra-class"},[a("pre",{pre:!0,attrs:{class:"language-js"}},[a("code",[a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"graphql"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"enabled"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token boolean"}},[t._v("true")]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"route"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"/graphql"')]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])]),a("p",[t._v("Options:")]),t._v(" "),a("ul",[a("li",[a("code",[t._v("enabled")]),t._v(" (boolean): Enabled or disabled GraphQL.")]),t._v(" "),a("li",[a("code",[t._v("route")]),t._v(" (string): Change GraphQL endpoint.")])]),t._v(" "),a("p",[t._v("Note: If GraphQL is disabled, the GraphQL global variable is not exposed.")]),t._v(" "),a("h2",{attrs:{id:"execute-simple-query"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#execute-simple-query","aria-hidden":"true"}},[t._v("#")]),t._v(" Execute simple query")]),t._v(" "),a("h3",{attrs:{id:"programmatically"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#programmatically","aria-hidden":"true"}},[t._v("#")]),t._v(" Programmatically")]),t._v(" "),a("p",[t._v("Strapi takes over GraphQL natively. We added a function called "),a("code",[t._v("query")]),t._v(" to execute\nyour query without given as a parameters the GraphQL schemas each time.")]),t._v(" "),a("p",[t._v("An example of how to use "),a("code",[t._v("query")]),t._v(" function:")]),t._v(" "),a("div",{staticClass:"language-js extra-class"},[a("pre",{pre:!0,attrs:{class:"language-js"}},[a("code",[a("span",{attrs:{class:"token comment"}},[t._v("// Build your query")]),t._v("\n"),a("span",{attrs:{class:"token keyword"}},[t._v("const")]),t._v(" query "),a("span",{attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v("'{ users{firstName lastName posts{title}} }'")]),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n\n"),a("span",{attrs:{class:"token comment"}},[t._v("// Execute the query")]),t._v("\ngraphql"),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),a("span",{attrs:{class:"token function"}},[t._v("query")]),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),t._v("query"),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),a("span",{attrs:{class:"token function"}},[t._v("then")]),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),a("span",{attrs:{class:"token keyword"}},[t._v("function")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),t._v("result"),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n console"),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),a("span",{attrs:{class:"token function"}},[t._v("log")]),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),t._v("result"),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),a("span",{attrs:{class:"token keyword"}},[t._v("catch")]),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),a("span",{attrs:{class:"token keyword"}},[t._v("function")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),t._v("error"),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n console"),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),a("span",{attrs:{class:"token function"}},[t._v("log")]),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),t._v("error"),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n")])])]),a("p",[t._v("And the JSON result:")]),t._v(" "),a("div",{staticClass:"language-js extra-class"},[a("pre",{pre:!0,attrs:{class:"language-js"}},[a("code",[a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"users"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("[")]),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"firstname"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"John"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"lastname"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"Doe"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"posts"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),a("span",{attrs:{class:"token punctuation"}},[t._v("[")]),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"title"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"First title..."')]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"title"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"Second title..."')]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"title"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"Third title..."')]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),a("span",{attrs:{class:"token punctuation"}},[t._v("]")]),t._v(" \n "),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"firstname"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"Karl"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"lastname"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"Doe"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"posts"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),a("span",{attrs:{class:"token punctuation"}},[t._v("[")]),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),a("span",{attrs:{class:"token string"}},[t._v('"title"')]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('"Fourth title..."')]),t._v("\n "),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),a("span",{attrs:{class:"token punctuation"}},[t._v("]")]),t._v(" \n "),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),a("span",{attrs:{class:"token punctuation"}},[t._v("]")]),t._v("\n"),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])]),a("h3",{attrs:{id:"with-a-http-request"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#with-a-http-request","aria-hidden":"true"}},[t._v("#")]),t._v(" With a HTTP request")]),t._v(" "),a("p",[t._v("Strapi also provides a HTTP GraphQL server to execute request from your front-end application.")]),t._v(" "),a("p",[t._v("An example of how to execute the same request as above with a HTTP request with jQuery.")]),t._v(" "),a("div",{staticClass:"language-js extra-class"},[a("pre",{pre:!0,attrs:{class:"language-js"}},[a("code",[t._v("$"),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),a("span",{attrs:{class:"token keyword"}},[t._v("get")]),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),a("span",{attrs:{class:"token string"}},[t._v("'http://yourserver.com/graphql?query={ users{firstName lastName posts{title}} }'")]),a("span",{attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),a("span",{attrs:{class:"token keyword"}},[t._v("function")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),t._v("data"),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n console"),a("span",{attrs:{class:"token punctuation"}},[t._v(".")]),a("span",{attrs:{class:"token function"}},[t._v("log")]),a("span",{attrs:{class:"token punctuation"}},[t._v("(")]),t._v("data"),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),a("span",{attrs:{class:"token punctuation"}},[t._v(")")]),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n")])])]),a("h2",{attrs:{id:"execute-complex-queries"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#execute-complex-queries","aria-hidden":"true"}},[t._v("#")]),t._v(" Execute complex queries")]),t._v(" "),a("h3",{attrs:{id:"query-parameters"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#query-parameters","aria-hidden":"true"}},[t._v("#")]),t._v(" Query parameters")]),t._v(" "),a("p",[t._v("If you're using Waterline ORM installed by default with Strapi, you have access to\nsome Waterline query parameters in your GraphQL query such as "),a("code",[t._v("sort")]),t._v(", "),a("code",[t._v("limit")]),t._v(" or "),a("code",[t._v("skip")]),t._v(".\nStrapi also provides the "),a("code",[t._v("start")]),t._v(" and "),a("code",[t._v("end")]),t._v(" parameters to select records between two dates.")]),t._v(" "),a("p",[t._v("This example will return 10 users' records sorted alphabetically by "),a("code",[t._v("firstName")]),t._v(":")]),t._v(" "),a("div",{staticClass:"language-js extra-class"},[a("pre",{pre:!0,attrs:{class:"language-js"}},[a("code",[a("span",{attrs:{class:"token keyword"}},[t._v("const")]),t._v(" query "),a("span",{attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v("'{ users(limit: 10, sort: \"firstName ASC\"){firstName lastName post{title}} }'")]),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n")])])]),a("p",[t._v("You can access to the 10 next users by adding the "),a("code",[t._v("skip")]),t._v(" parameter:")]),t._v(" "),a("div",{staticClass:"language-js extra-class"},[a("pre",{pre:!0,attrs:{class:"language-js"}},[a("code",[a("span",{attrs:{class:"token keyword"}},[t._v("const")]),t._v(" query "),a("span",{attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v("'{ users(limit: 10, sort: \"firstName ASC\", skip: 10){firstName lastName posts{title}} }'")]),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n")])])]),a("p",[t._v("And you also can select those records in a period between two dates with the "),a("code",[t._v("start")]),t._v(" and "),a("code",[t._v("end")]),t._v(" parameters:")]),t._v(" "),a("div",{staticClass:"language-js extra-class"},[a("pre",{pre:!0,attrs:{class:"language-js"}},[a("code",[a("span",{attrs:{class:"token keyword"}},[t._v("const")]),t._v(" query "),a("span",{attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),a("span",{attrs:{class:"token string"}},[t._v('\'{ users(limit: 10, sort: "firstName ASC", skip: 10, start: "09/21/2015", end:" 09/22/2015"){firstName lastName posts{title}} }\'')]),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n")])])]),a("h3",{attrs:{id:"useful-functions"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#useful-functions","aria-hidden":"true"}},[t._v("#")]),t._v(" Useful functions")]),t._v(" "),a("p",[t._v("Strapi comes with a powerful set of useful functions such as "),a("code",[t._v("getLatest