Configuring the REST API
The @perseid/server package is a very powerful way to generate a fully functional REST API from your data model. It automatically provides the most common features such as authentication, resources CRUD, and more.
While the Engine
is responsible for managing all the business logic, and orchestrating the different services (sending emails, reading and writing from databases, and such), the Controller
is responsible for exposing this engine over the Internet, using the framework of your choice.
Controller Settings
Here is an example of a Controller settings:
{
// This is the backend version. It is sent always along with API responses through the
// "X-Api-Version" header. This information is especially useful to let the frontend know that the
// API has been updated, and that it should be reloaded to prevent any incompatibility errors
// for instance.
version: '0.0.1',
// When this parameter is enabled, the server will automatically handle preflight HTTP requests,
// and CORS headers like "Access-Control-Allow-Origin". This should be mainly used in development.
handleCORS: true,
// This part is where you define the built-in API endpoints that should be exposed. There are two
// categories of built-in endpoints: authentication-related endpoints, and resources endpoints.
// Auth endpoints are responsible for managing the whole authentication system, including sign-up,
// token refresh, sign-out, and such. Resources endpoints handle classic CRUD operations on your
// data model.
// All endpoints are optional. By default, no endpoint is available, and you need to enable them
// one by one.
endpoints: {
auth: {
viewMe: { path: '/auth/me' },
signUp: { path: '/auth/sign-up' },
signIn: { path: '/auth/sign-in' },
},
resources: {
roles: {
// `path` defines the API path for that endpoint.
// `maximumDepth` defines the maximum depth level for nested relations in your data model
// (see below).
list: { path: '/roles', maximumDepth: 6 },
create: { path: '/roles' },
view: { path: '/roles/:id' },
update: { path: '/roles/:id' },
}
}
}
}
Built-in authentication endpoints
signUp
Handles users sign-up.
- HTTP method:
POST
- Required body: Sign-up JSON payload
Typical sign-up JSON payload example:
{
"email": "test@test.test",
"password": "Test123!",
"passwordConfirmation": "Test123!"
}
Typical response structure example:
{
"deviceId": "d13b0b02cc43e982f8e61856",
"expiresIn": 1200,
"refreshTokenExpiration": "2024-11-29T09:07:34.508Z",
"refreshToken": "02a7d06a642dc49bf18bab8f",
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzAyNzkyNTQsImV4cCI6MTczMDI4MDQ1NCwiYXVkIjoiZXhhbXBsZSIsImlzcyI6ImV4YW1wbGUiLCJzdWIiOiI2NzIxZjc1NjY1NDFlYWMyYTlkNTM2Y2RfZDEzYjBiMDJjYzQzZTk4MmY4ZTYxODU2In0.fpp6HzOCb3JTZwxxqpjawd9fzbZ1xULVelXeO5y-k5jtw_dXIUtWJdq5Ew5M2E6EKyEmHFJqCcyj9Rm9tDqqVY719eDGU5IEmCu_L0Q2jqhKRSpXCycKI8WugT4goHPFOAV-kHj-c08H-XIEFV6oZKG5uVnxp-6xTMs0xnhAzjFOw22l-vQ7nZyKnBna-SCH3FunOp4yM2PwAK2RirOjPKuTJ6T4LAhzqRNhVpzpMjVWUFR9dgpCk4eMIe2QO_HsXQBo_jH0LQkh3ASH7aAUdBRZyk_f56g1G4ltZrlqIY7jlH2F-zYPEoB-tGOvd7CKz4DvG9n3lk1VXBpoJo-RKPLkbtZtW_azlfXFYGcNiGmO20D5FE703XSuJR4PjQjmiLCRV5QUHTk8ox-VPyhB_eudaw8TaarD-epRQjkz5U2QAKa3CBnE29xiZo-PJXLSlcmd9OzXGmQ8nAjkKevhWb2SEuRQ6z78O85IWVUtzx6wL1oseFeGQmzH5sGL-7V0"
}
signIn
Handles users sign-in.
- HTTP method:
POST
- Required body: Sign-in JSON payload
- Optional headers:
X-Device-Id
Typical sign-in JSON payload example:
{
"email": "test@test.test",
"password": "Test123!",
}
Typical response structure example:
{
"deviceId": "d13b0b02cc43e982f8e61856",
"expiresIn": 1200,
"refreshTokenExpiration": "2024-11-29T09:07:34.508Z",
"refreshToken": "02a7d06a642dc49bf18bab8f",
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzAyNzkyNTQsImV4cCI6MTczMDI4MDQ1NCwiYXVkIjoiZXhhbXBsZSIsImlzcyI6ImV4YW1wbGUiLCJzdWIiOiI2NzIxZjc1NjY1NDFlYWMyYTlkNTM2Y2RfZDEzYjBiMDJjYzQzZTk4MmY4ZTYxODU2In0.fpp6HzOCb3JTZwxxqpjawd9fzbZ1xULVelXeO5y-k5jtw_dXIUtWJdq5Ew5M2E6EKyEmHFJqCcyj9Rm9tDqqVY719eDGU5IEmCu_L0Q2jqhKRSpXCycKI8WugT4goHPFOAV-kHj-c08H-XIEFV6oZKG5uVnxp-6xTMs0xnhAzjFOw22l-vQ7nZyKnBna-SCH3FunOp4yM2PwAK2RirOjPKuTJ6T4LAhzqRNhVpzpMjVWUFR9dgpCk4eMIe2QO_HsXQBo_jH0LQkh3ASH7aAUdBRZyk_f56g1G4ltZrlqIY7jlH2F-zYPEoB-tGOvd7CKz4DvG9n3lk1VXBpoJo-RKPLkbtZtW_azlfXFYGcNiGmO20D5FE703XSuJR4PjQjmiLCRV5QUHTk8ox-VPyhB_eudaw8TaarD-epRQjkz5U2QAKa3CBnE29xiZo-PJXLSlcmd9OzXGmQ8nAjkKevhWb2SEuRQ6z78O85IWVUtzx6wL1oseFeGQmzH5sGL-7V0"
}
signOut
Handles users sign-out.
- HTTP method:
POST
- Required headers:
Authorization
,X-Device-Id
Typical response structure example:
refreshToken
Handles users access token refresh.
- HTTP method:
POST
- Required body: Token refresh JSON payload
- Required headers:
Authorization
,X-Device-Id
Typical token refresh JSON payload example:
{
// This token is returned in the `refreshToken` field of the `signIn` and `refreshToken` API endpoints.
"refreshToken": "02a7d06a642dc49bf18bab8f"
}
Typical response structure example:
{
"deviceId": "d13b0b02cc43e982f8e61856",
"expiresIn": 1200,
"refreshTokenExpiration": "2024-11-29T09:07:34.508Z",
"refreshToken": "02a7d06a642dc49bf18bab8f",
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzAyNzkyNTQsImV4cCI6MTczMDI4MDQ1NCwiYXVkIjoiZXhhbXBsZSIsImlzcyI6ImV4YW1wbGUiLCJzdWIiOiI2NzIxZjc1NjY1NDFlYWMyYTlkNTM2Y2RfZDEzYjBiMDJjYzQzZTk4MmY4ZTYxODU2In0.fpp6HzOCb3JTZwxxqpjawd9fzbZ1xULVelXeO5y-k5jtw_dXIUtWJdq5Ew5M2E6EKyEmHFJqCcyj9Rm9tDqqVY719eDGU5IEmCu_L0Q2jqhKRSpXCycKI8WugT4goHPFOAV-kHj-c08H-XIEFV6oZKG5uVnxp-6xTMs0xnhAzjFOw22l-vQ7nZyKnBna-SCH3FunOp4yM2PwAK2RirOjPKuTJ6T4LAhzqRNhVpzpMjVWUFR9dgpCk4eMIe2QO_HsXQBo_jH0LQkh3ASH7aAUdBRZyk_f56g1G4ltZrlqIY7jlH2F-zYPEoB-tGOvd7CKz4DvG9n3lk1VXBpoJo-RKPLkbtZtW_azlfXFYGcNiGmO20D5FE703XSuJR4PjQjmiLCRV5QUHTk8ox-VPyhB_eudaw8TaarD-epRQjkz5U2QAKa3CBnE29xiZo-PJXLSlcmd9OzXGmQ8nAjkKevhWb2SEuRQ6z78O85IWVUtzx6wL1oseFeGQmzH5sGL-7V0"
}
requestPasswordReset
Handles users password reset requests.
- HTTP method:
POST
- Required body: Password reset JSON payload
Typical password reset JSON payload example:
{
"email": "test@test.test"
}
Typical response structure example:
resetPassword
Handles users password replacement by a new one after they requested a password reset.
- HTTP method:
PUT
- Required body: Password reset JSON payload
Typical password reset JSON payload example:
{
"resetToken": "02a7d06a642dc49bf18bab8f",
"password": "Test123!",
"passwordConfirmation": "Test123!"
}
Typical response structure example:
requestEmailVerification
Handles users email verification requests.
- HTTP method:
POST
- Required headers:
Authorization
,X-Device-Id
Typical response structure example:
verifyEmail
Handles users email verification after they signed-up or requested an email verification.
- HTTP method:
PUT
- Required body: Email verification JSON payload
- Required headers:
Authorization
,X-Device-Id
Typical email verification JSON payload example:
{
"verificationToken": "02a7d06a642dc49bf18bab8f"
}
Typical response structure example:
viewMe
Handles users profile view.
- HTTP method:
GET
- Required headers:
Authorization
,X-Device-Id
Typical response structure example:
{
"_id": "000000000000000000000123",
"email": "test@test.test",
"_createdAt": "2023-01-01T00:00:00.000Z",
"_updatedAt": "2023-01-02T00:00:00.000Z",
"_verifiedAt": "2023-01-02T00:00:00.000Z",
"roles": [
{
"name": "ADMIN",
"permissions": [
"ROLES_VIEW"
]
}
],
}
Built-in resources endpoints
create
Handles specific resource creation.
- HTTP method:
POST
- Required headers:
Authorization
,X-Device-Id
- Optional query params:
fields
- Required body: A JSON payload that depends on the type of resource to create
Typical response structure example:
{
"_id": "000000000000000000000001",
"_createdAt": "2023-01-01T00:00:00.000Z",
"_createdBy": "000000000000000000000123",
"name": "ADMIN",
"permissions": [
"ROLES_VIEW"
],
// ...
}
view
Handles specific resource fetching.
- HTTP method:
GET
- Required headers:
Authorization
,X-Device-Id
- Optional query params:
fields
- Required URL params:
id
Typical response structure example:
{
"_id": "000000000000000000000001",
"_createdAt": "2023-01-01T00:00:00.000Z",
"_createdBy": "000000000000000000000123",
"name": "ADMIN",
"permissions": [
"ROLES_VIEW"
],
// ...
}
update
Handles specific resource update.
- HTTP method:
PUT
- Required headers:
Authorization
,X-Device-Id
- Optional query params:
fields
- Required URL params:
id
- Required body: A JSON payload that depends on the type of resource to create
Typical response structure example:
{
"_id": "000000000000000000000001",
"_createdAt": "2023-01-01T00:00:00.000Z",
"_createdBy": "000000000000000000000123",
"name": "ADMIN",
"permissions": [
"ROLES_VIEW"
],
// ...
}
list
Handles resources listing.
- HTTP method:
GET
- Required headers:
Authorization
,X-Device-Id
- Optional query params:
limit
,offset
,sortBy
,sortOrder
,fields
Typical response structure example:
{
"total": "13",
"results": [
{
"_id": "000000000000000000000001",
"_createdAt": "2023-01-01T00:00:00.000Z",
"_createdBy": "000000000000000000000123",
"name": "ADMIN",
"permissions": [
"ROLES_VIEW"
],
// ...
},
{
"_id": "000000000000000000000002",
"_createdAt": "2023-01-02T00:00:00.000Z",
"_createdBy": "000000000000000000000123",
"name": "EDITOR",
"permissions": [
"ROLES_UPDATE"
],
// ...
},
// ...
]
}
search
Handles resources search according to specific criteria.
- HTTP method:
POST
- Required headers:
Authorization
,X-Device-Id
- Optional query params:
limit
,offset
,sortBy
,sortOrder
,fields
- Required body: A JSON search payload containing search criteria
Typical search payload structure:
{
// Allows full-text search on specific fields.
"query": {
"on": ["name", "permissions"],
"text": "test"
},
// Strict filters to apply to results.
"filters": {
"_createdBy": "000000000000000000000123",
"_createdAt": ["2021-01-01T00:00:00.000Z", "2025-01-01T00:00:00.000Z"]
}
}
Typical response structure example:
{
"total": "13",
"results": [
{
"_id": "000000000000000000000001",
"_createdAt": "2023-01-01T00:00:00.000Z",
"_createdBy": "000000000000000000000123",
"name": "ADMIN",
"permissions": [
"ROLES_VIEW"
],
// ...
},
{
"_id": "000000000000000000000002",
"_createdAt": "2023-01-02T00:00:00.000Z",
"_createdBy": "000000000000000000000123",
"name": "EDITOR",
"permissions": [
"ROLES_UPDATE"
],
// ...
},
// ...
]
}
delete
Handles resource deletion.
- HTTP method:
DELETE
- Required URL params:
id
- Required headers:
Authorization
,X-Device-Id
Typical response structure example:
Built-in automatic endpoints
Automatic endpoints are internal endpoints providing essential meta-data about the API and the underlying data model. They are always enabled,and their path cannot be manually configured.
/_model
Returns a fragment of the data model schema, containing the requested resource and all related data model schemas.
- HTTP method:
GET
- Required query params:
resource
- Required headers:
Authorization
,X-Device-Id
Typical response structure example:
{
"roles": {
"fields": {
"name": {
"type": "string",
"isRequired": true,
// ...
},
// ...
}
},
"users": {
"fields": {
"email": {
"type": "string",
"isRequired": true,
// ...
},
// ...
}
}
}
Built-in headers
-
X-Device-Id
: Id of the device that is making the request. If not provided, a new one will be generated on user sign-in and returned in thedeviceId
field. When using@perseid/client
, this header is automatically managed. It allows you to control the origins you want to allow. -
Authorization
: Contains the JWT Bearer token that is generated on user sign-in. Used to authentify user on every request.
Partial vs Full payloads
Partial payloads are a powerful way to reduce the size of requests payloads and the necessary amount network traffic. This can have a significant impact on the performance of your application, especially on low-performance devices or in network constrained environments.
- When creating a new resource, you are required to pass all its fields in the payload.
- When updating a resource, you can only pass the fields that you want to update. A few things to keep in mind though:
- For arrays, you need to pass all the array items in the payload.
- For optional objects or arrays, you need to pass the whole object or array in the payload.
Automatic fields
Automatic fields is a design choice made by Perseid to make resources management and learning easier, without needing much documentation. It relies on a very simple concept: each field prefixed by _
is considered an automatic field. This means that the user cannot define it directly (either through the API or the Engine directly). This field will be automatically generated inside the Engine, according to a specific business logic. For instance, the _createdAt
field is not updatable directly by the user, and is automatically set to the current time when the resource is created.
This way, developers can have a clear vision of what they can change, and what they cannot.
Built-in query params
limit
Specifies the maximum number of results to fetch. Available on search
and list
endpoints.
offset
Specifies the number of results to skip. Available on search
and list
endpoints.
sortBy
Specifies the field to sort results by. sortOrder
is also required when using this query parameter. Available on search
and list
endpoints.
For example: /users?sortBy=email,_createdAt&sortOrder=1,-1
sortOrder
Specifies the order to sort results. sortBy
is also required when using this query parameter. Available on search
and list
endpoints.
For example: /users?sortBy=email,_createdAt&sortOrder=1,-1
fields
Specifies the list of fields to fetch from the API. Available on view
, create
, update
, search
and list
endpoints.
This query parameter is one of the most powerful features of Perseid. It allows developers to get a fine control of what they need to fetch from both the API and the underlying database, which can have a significant impact on the performance of your application, especially on low-performance devices or in network constrained environments.
- The
*
special value means "all fields". You can use that value either on the complete resource (fields=*
), on a sub-resource (fields=_updatedBy.*
), or on anobject
orarray
field (fields=devices.*
). - You can walk through the whole data model and nested resources with the
fields
query parameter. For instance, to get the email address of the user that updated the author of a new role, you could do:fields=_updatedBy._createdBy.email
. Perseid will automatically know which fields to fetch, and generate a single optimized database query to fetch exactly these fields. The maximum level of depth you can go is defined at each endpoint level with themaximumDepth
configuration option (defaults to3
).
Connecting Controller to your framework
Once your Controller is instanciated and configured, you can connect it with the most popular frameworks like express
or fastify
.
All you have to do is define and start your server like you would usually do, and call the Controller.createEndpoints
method.
For instance, to connect your Controller to ExpressJs:
import express from 'express';
import { ExpressController } from '@perseid/server/express';
// Declaring your Controller...
const controller = new ExpressController({
version: '1.0.0',
handleCORS: true,
endpoints: {
// Your built-inendpoints here...
auth: {},
resources: {},
}
});
// Creating our ExpressJS server...
const app = express();
// Declaring other endpoints here...
app.get('/hello', (_request, response) => {
response.status(200).send('HELLO WORLD!');
});
// Registering all Perseid built-in endpoints...
await controller.createEndpoints(app, { prefix: '/perseid' });
// And starting the ExpressJS server!
app.listen(3000);
Declaring custom API endpoints
If you need to register fully custom API endpoints, you can do so through the Controller.createEndpoint
method.