Skip to content

Why use Plateforme?

Plateforme lets you build powerful data-driven applications and services with minimal effort. Its resource-centric architecture features a natural, intuitive syntax that prioritizes developer experience, enabling extremely fast prototyping while maintaining extensive customization options, high performance and scalability.

Unlike traditional frameworks that require piecing together multiple components, Plateforme unifies your data models and schemas, API endpoints, and business logic into simple, declarative resources and services. This opinionated approach eliminates boilerplate code and creates predictable patterns, making it also an ideal foundation for AI-assisted development where complex applications can be generated from minimal, precise specifications.

The documentation is under heavy development!

The documentation is actively being developed and might not cover every feature. For full details, refer to the source code.

In actions

A minimal example

Let's explore a practical implementation of a rocket factory system that demonstrates Plateforme's resource-centric architecture and its ability to handle complex relationships with minimal code:

from plateforme import CRUDResource, Field

class Material(CRUDResource):  # (1)!
    name: str

class Rocket(CRUDResource):
    name: str
    parts: list[Material] = Field(default_factory=list)  # (2)!
  1. The Material resource inherits from CRUDResource, automatically providing a complete set of create, read, update, upsert, and delete operations without additional implementation.
  2. The parts field uses default_factory=list to ensure each rocket instance receives its own unique list of materials, preventing shared state between different instances. This is a crucial Python best practice for mutable default values.
Understanding the difference between Base and CRUD resources

Plateforme uses a service-oriented architecture where resources can subscribe to different services that define their behavior. The BaseResource class provides core resource functionality, while the CRUDResource extends it by subscribing to the built-in CRUDService. This service implements standard create, read, update, upsert, and delete operations, making it immediately ready for use.

The following examples showcase how to use CRUDResource classes effectively. The CRUDService can be configured to provide additional operations or restrict access to default ones, serving as both a complete solution for common use cases and a reference implementation for custom services.

Beyond the built-in functionality, Plateforme enables you to define custom services for your resources, allowing you to encapsulate specific business logic or behavior.

Schema

This straightforward definition generates a database schema that automatically handles the identification strategy and polymorphism used for class inheritance through the id and type fields. Primary key can be configured to use either integers or UUIDs, where the value can be either generated automatically or provided by the user depending on the specified resource configuration. The generated schema also captures the many-to-many relationship between rockets and materials:

erDiagram
    material {
        int id PK
        string type
        string name
    }
    rocket {
        int id PK
        string type
        string name
    }
    rocket_material {
        int rocket_id PK, FK
        int material_id PK, FK
    }

    material ||--o{ rocket_material : "used in"
    rocket ||--o{ rocket_material : "composed of"

Endpoints

For resources that subscribe to the CRUDService, it will generate a set of RESTful CRUD endpoints that allow you to interact with the data:

  • GET  Read operations


    • /materials → Retrieve multiple materials
    • /materials/{_material_key} → Retrieve a specific material
    • /rockets → Retrieve multiple rockets
    • /rockets/{_rocket_key} → Retrieve a specific rocket
  • POST  Create operations


    • /materials → Create new materials
    • /rockets → Create new rockets
  • PATCH  Update operations


    • /materials → Update multiple materials
    • /materials/{_material_key} → Update a specific material
    • /rockets → Update multiple rockets
    • /rockets/{_rocket_key} → Update a specific rocket
  • PUT  Upsert operations


    • /materials → Create or update materials
    • /rockets → Create or update rockets
  • DELETE  Delete operations


    • /materials → Delete multiple materials
    • /materials/{_material_key} → Delete a specific material
    • /rockets → Delete multiple rockets
    • /rockets/{_rocket_key} → Delete a specific rocket

A better example

To enhance our model with additional relationship attributes, such as tracking the quantity of materials used in each rocket, we can introduce a RocketPart resource:

from plateforme import ConfigDict, CRUDResource, Field

class Material(CRUDResource):
    code: str = Field(unique=True)
    name: str
    rocket_parts: list['RocketPart'] = Field(default_factory=list)  # (1)!

class Rocket(CRUDResource):
    code: str = Field(unique=True)
    name: str
    parts: list['RocketPart'] = Field(default_factory=list)  # (2)!

class RocketPart(CRUDResource):
    __config__ = ConfigDict(indexes=[{'rocket', 'material'}])  # (3)!

    rocket: Rocket  # (4)!
    material: Material
    quantity: int
  1. The rocket_parts field establishes a reverse relationship to RocketPart, enabling bidirectional navigation between materials and their usage in rockets.
  2. The parts field now references RocketPart instead of Material directly, allowing for additional attributes on the relationship.
  3. A composite index ensures uniqueness of material-rocket combinations, preventing duplicate entries.
  4. Forward references using string literals establish proper relationships while avoiding circular imports.

Schema

This enhanced model generates an expanded database schema with additional fields to capture the relationship between rockets, materials, and their quantities:

erDiagram
    material {
        int id PK
        string type
        string code UK
        string name
    }
    rocket {
        int id PK
        string type
        string code UK
        string name
    }
    rocket_part {
        int id PK
        string type
        int rocket_id UK, FK
        int material_id UK, FK
        float quantity
    }

    material ||--o{ rocket_part : "used in"
    rocket ||--o{ rocket_part : "composed of"

Endpoints

This will generate additional endpoints to manage the new relationships. The framework can automatically infer nested endpoint trees based on the resource relationships, the depth of which can be configured at both the application, package, and resource levels:

  • GET  Added read operations


    • Extends previous endpoints with...
    • /materials/{_material_key}/rocket-parts → List all uses of a material
    • /rockets/{_rocket_key}/parts → List all parts of a rocket
    • /rocket-parts/{_rocket_part_key}/material → Get used material
    • /rocket-parts/{_rocket_part_key}/rocket → Get parent rocket
  • POST  Added create operations


    • Extends previous endpoints with...
    • /materials/{_material_key}/rocket-parts → Create new material usage
    • /rockets/{_rocket_key}/parts → Add parts to a rocket
  • PATCH  Added update operations


    • Extends previous endpoints with...
    • /materials/{_material_key}/rocket-parts → Update material usages
    • /rockets/{_rocket_key}/parts → Update multiple parts
    • /rocket-parts/{_rocket_part_key}/material → Update part's material
    • /rocket-parts/{_rocket_part_key}/rocket → Update part's rocket
  • PUT  Added upsert operations


    • Extends previous endpoints with...
    • /materials/{_material_key}/rocket-parts → Create or update material usage
    • /rockets/{_rocket_key}/parts → Create or update rocket parts
  • DELETE  Added delete operations


    • Extends previous endpoints with...
    • /materials/{_material_key}/rocket-parts → Remove material usages
    • /rockets/{_rocket_key}/parts → Remove multiple parts
    • /rocket-parts/{_rocket_part_key}/material → Remove material from part
    • /rocket-parts/{_rocket_part_key}/rocket → Remove part from rocket

Playing with the API

Materials

Add some materials

Let's start by creating some materials. Each CRUD method exposes a set of arguments that can be used for instance to filter, sort, and paginate the results. The POST endpoint bellow expects a JSON payload with the material's name and code. This will create 16 materials in total:

Create some materials
POST http://127.0.0.1:8001/materials HTTP/1.1
content-type: application/json

{
  "payload": [
    {
      "name": "Main Engine",
      "code": "ME-1000"
    },
    {
      "name": "Primary Fuel Tank",
      "code": "FT-500"
    },
    ...
  ]
}
Request
POST http://127.0.0.1:8001/materials HTTP/1.1
content-type: application/json

{
  "payload": [
    {
      "name": "Main Engine",
      "code": "ME-1000"
    },
    {
      "name": "Primary Fuel Tank",
      "code": "FT-500"
    },
    {
      "name": "Secondary Fuel Tank",
      "code": "FT-501"
    },
    {
      "name": "Guidance System",
      "code": "GS-200"
    },
    {
      "name": "Primary Payload Bay",
      "code": "PB-300"
    },
    {
      "name": "Secondary Payload Bay",
      "code": "PB-301"
    },
    {
      "name": "Nose Cone",
      "code": "NC-150"
    },
    {
      "name": "Thrust Vector Control",
      "code": "TV-400"
    },
    {
      "name": "Navigation Computer",
      "code": "NC-600"
    },
    {
      "name": "Communication System",
      "code": "CS-700"
    },
    {
      "name": "Thermal Protection System",
      "code": "TP-800"
    },
    {
      "name": "Landing Gear",
      "code": "LG-250"
    },
    {
      "name": "Power Distribution Unit",
      "code": "PD-900"
    },
    {
      "name": "Life Support System",
      "code": "LS-350"
    },
    {
      "name": "Emergency Escape System",
      "code": "EE-450"
    },
    {
      "name": "Oxidizer Tank",
      "code": "OT-550"
    }
  ]
}
Response
[
  {
    "id": 1,
    "type": "material",
    "code": "ME-1000",
    "name": "Main Engine",
    "rocket_parts": []
  },
  {
    "id": 2,
    "type": "material",
    "code": "FT-500",
    "name": "Primary Fuel Tank",
    "rocket_parts": []
  },
  {
    "id": 3,
    "type": "material",
    "code": "FT-501",
    "name": "Secondary Fuel Tank",
    "rocket_parts": []
  },
  {
    "id": 4,
    "type": "material",
    "code": "GS-200",
    "name": "Guidance System",
    "rocket_parts": []
  },
  {
    "id": 5,
    "type": "material",
    "code": "PB-300",
    "name": "Primary Payload Bay",
    "rocket_parts": []
  },
  {
    "id": 6,
    "type": "material",
    "code": "PB-301",
    "name": "Secondary Payload Bay",
    "rocket_parts": []
  },
  {
    "id": 7,
    "type": "material",
    "code": "NC-150",
    "name": "Nose Cone",
    "rocket_parts": []
  },
  {
    "id": 8,
    "type": "material",
    "code": "TV-400",
    "name": "Thrust Vector Control",
    "rocket_parts": []
  },
  {
    "id": 9,
    "type": "material",
    "code": "NC-600",
    "name": "Navigation Computer",
    "rocket_parts": []
  },
  {
    "id": 10,
    "type": "material",
    "code": "CS-700",
    "name": "Communication System",
    "rocket_parts": []
  },
  {
    "id": 11,
    "type": "material",
    "code": "TP-800",
    "name": "Thermal Protection System",
    "rocket_parts": []
  },
  {
    "id": 12,
    "type": "material",
    "code": "LG-250",
    "name": "Landing Gear",
    "rocket_parts": []
  },
  {
    "id": 13,
    "type": "material",
    "code": "PD-900",
    "name": "Power Distribution Unit",
    "rocket_parts": []
  },
  {
    "id": 14,
    "type": "material",
    "code": "LS-350",
    "name": "Life Support System",
    "rocket_parts": []
  },
  {
    "id": 15,
    "type": "material",
    "code": "EE-450",
    "name": "Emergency Escape System",
    "rocket_parts": []
  },
  {
    "id": 16,
    "type": "material",
    "code": "OT-550",
    "name": "Oxidizer Tank",
    "rocket_parts": []
  }
]

Fetch materials

We can now retrieve the list of materials we just created and use built-in filtering and sorting capabilities to narrow down the results. The following request fetches the first 5 materials sorted by code in descending order:

Fetch materials with sorting and limit
GET http://127.0.0.1:8001/materials?limit=5&sort=-code HTTP/1.1
Response
[
  {
    "id": 8,
    "type": "material",
    "code": "TV-400",
    "name": "Thrust Vector Control",
    "rocket_parts": []
  },
  {
    "id": 11,
    "type": "material",
    "code": "TP-800",
    "name": "Thermal Protection System",
    "rocket_parts": []
  },
  {
    "id": 13,
    "type": "material",
    "code": "PD-900",
    "name": "Power Distribution Unit",
    "rocket_parts": []
  },
  {
    "id": 6,
    "type": "material",
    "code": "PB-301",
    "name": "Secondary Payload Bay",
    "rocket_parts": []
  },
  {
    "id": 5,
    "type": "material",
    "code": "PB-300",
    "name": "Primary Payload Bay",
    "rocket_parts": []
  }
]

Filter materials

We can also use the built-in query capabilities to fetch materials that match specific conditions. Multiple filters can be combined to create complex queries, each condition can be applied to specific fields, nested or not, using the dot . notation. For instance, the following request fetches materials whose code contains the string NC and have an id greater than 2:

Fetch materials that match specific conditions
GET http://127.0.0.1:8001/materials?.code=like~*NC*&.id=gt~2 HTTP/1.1
Response
[
  {
    "id": 7,
    "type": "material",
    "code": "NC-150",
    "name": "Nose Cone",
    "rocket_parts": []
  },
  {
    "id": 9,
    "type": "material",
    "code": "NC-600",
    "name": "Navigation Computer",
    "rocket_parts": []
  }
]

Rockets

Add some rockets

Now that we have created some materials, let's create a few rockets. We will start by creating simple rockets without any material parts associated:

Create some rockets without parts
POST http://127.0.0.1:8001/rockets HTTP/1.1
content-type: application/json

{
  "payload": [
    {
      "name": "Saturn V",
      "code": "SAT-V"
    },
    {
      "name": "Falcon 9",
      "code": "FAL-9"
    },
    {
      "name": "Space Launch System",
      "code": "SLS-1"
    }
  ]
}
Response
[
  {
    "id": 1,
    "type": "rocket",
    "code": "SAT-V",
    "name": "Saturn V",
    "parts": []
  },
  {
    "id": 2,
    "type": "rocket",
    "code": "FAL-9",
    "name": "Falcon 9",
    "parts": []
  },
  {
    "id": 3,
    "type": "rocket",
    "code": "SLS-1",
    "name": "Space Launch System",
    "parts": []
  }
]

We can also create rockets and associate them with materials at the same time, where either a reference to an existing material or a new material can be provided. The following request creates two rockets, one associated with existing materials and another with new materials:

Create rockets with associated materials
POST http://127.0.0.1:8001/rockets HTTP/1.1
content-type: application/json

{
  "payload": [
    {
      "name": "Atlas V",
      "code": "ATL-6",
      "parts": [
        {
          "material": 3,
          "quantity": 1
        },
        ...
      ]
    },
    {
      "name": "Ariane 6",
      "code": "ARI-6",
      "parts": [
        {
          "material": {
            "name": "Attitude Control System",
            "code": "AC-650"
          },
          "quantity": 2
        },
        ...
      ]
    }
  ]
}
Request
POST http://127.0.0.1:8001/rockets HTTP/1.1
content-type: application/json

{
  "payload": [
    {
      "name": "Atlas V",
      "code": "ATL-6",
      "parts": [
        {
          "material": 3,
          "quantity": 1
        },
        {
          "material": 4,
          "quantity": 1
        },
        {
          "material": 7,
          "quantity": 2
        },
        {
          "material": 9,
          "quantity": 5
        },
        {
          "material": 10,
          "quantity": 4
        },
        {
          "material": 11,
          "quantity": 1
        }
      ]
    },
    {
      "name": "Ariane 6",
      "code": "ARI-6",
      "parts": [
        {
          "material": {
            "name": "Attitude Control System",
            "code": "AC-650"
          },
          "quantity": 2
        },
        {
          "material": {
            "name": "Telemetry System",
            "code": "TS-750"
          },
          "quantity": 1
        },
        {
          "material": {
            "name": "Staging Mechanism",
            "code": "SM-850"
          },
          "quantity": 3
        },
        {
          "material": {
            "name": "Propellant Feed System",
            "code": "PF-950"
          },
          "quantity": 3
        }
      ]
    }
  ]
}
Response
[
  {
    "id": 4,
    "type": "rocket",
    "code": "ATL-6",
    "name": "Atlas V",
    "parts": [
      {
        "id": 1,
        "type": "rocket_part",
        "material": {
          "id": 3,
          "type": "material",
          "code": "FT-501",
          "name": "Secondary Fuel Tank"
        },
        "quantity": 1
      },
      {
        "id": 2,
        "type": "rocket_part",
        "material": {
          "id": 4,
          "type": "material",
          "code": "GS-200",
          "name": "Guidance System"
        },
        "quantity": 1
      },
      {
        "id": 3,
        "type": "rocket_part",
        "material": {
          "id": 7,
          "type": "material",
          "code": "NC-150",
          "name": "Nose Cone"
        },
        "quantity": 2
      },
      {
        "id": 4,
        "type": "rocket_part",
        "material": {
          "id": 9,
          "type": "material",
          "code": "NC-600",
          "name": "Navigation Computer"
        },
        "quantity": 5
      },
      {
        "id": 5,
        "type": "rocket_part",
        "material": {
          "id": 10,
          "type": "material",
          "code": "CS-700",
          "name": "Communication System"
        },
        "quantity": 4
      },
      {
        "id": 6,
        "type": "rocket_part",
        "material": {
          "id": 11,
          "type": "material",
          "code": "TP-800",
          "name": "Thermal Protection System"
        },
        "quantity": 1
      }
    ]
  },
  {
    "id": 5,
    "type": "rocket",
    "code": "ARI-6",
    "name": "Ariane 6",
    "parts": [
      {
        "id": 7,
        "type": "rocket_part",
        "material": {
          "id": 17,
          "type": "material",
          "code": "AC-650",
          "name": "Attitude Control System"
        },
        "quantity": 2
      },
      {
        "id": 8,
        "type": "rocket_part",
        "material": {
          "id": 18,
          "type": "material",
          "code": "TS-750",
          "name": "Telemetry System"
        },
        "quantity": 1
      },
      {
        "id": 9,
        "type": "rocket_part",
        "material": {
          "id": 19,
          "type": "material",
          "code": "SM-850",
          "name": "Staging Mechanism"
        },
        "quantity": 3
      },
      {
        "id": 10,
        "type": "rocket_part",
        "material": {
          "id": 20,
          "type": "material",
          "code": "PF-950",
          "name": "Propellant Feed System"
        },
        "quantity": 3
      }
    ]
  }
]

Automatic handling of recursive relationships

Plateforme automatically handles recursive relationships between resources. As you can see in the example above, we created rockets with associated materials, which in turn have a reference back to the rockets they are used in.

Fetch rockets

So far, we have created 5 rockets:

Fetch all rockets
GET http://127.0.0.1:8001/rockets HTTP/1.1
Response
[
  {
    "id": 1,
    "type": "rocket",
    "code": "SAT-V",
    "name": "Saturn V",
    "parts": []
  },
  {
    "id": 2,
    "type": "rocket",
    "code": "FAL-9",
    "name": "Falcon 9",
    "parts": []
  },
  {
    "id": 3,
    "type": "rocket",
    "code": "SLS-1",
    "name": "Space Launch System",
    "parts": []
  },
  {
    "id": 4,
    "type": "rocket",
    "code": "ATL-6",
    "name": "Atlas V",
    "parts": [
      {
        "id": 1,
        "type": "rocket_part",
        "material": {
          "id": 3,
          "type": "material",
          "code": "FT-501",
          "name": "Secondary Fuel Tank"
        },
        "quantity": 1
      },
      {
        "id": 2,
        "type": "rocket_part",
        "material": {
          "id": 4,
          "type": "material",
          "code": "GS-200",
          "name": "Guidance System"
        },
        "quantity": 1
      },
      {
        "id": 6,
        "type": "rocket_part",
        "material": {
          "id": 11,
          "type": "material",
          "code": "TP-800",
          "name": "Thermal Protection System"
        },
        "quantity": 1
      },
      {
        "id": 3,
        "type": "rocket_part",
        "material": {
          "id": 7,
          "type": "material",
          "code": "NC-150",
          "name": "Nose Cone"
        },
        "quantity": 2
      },
      {
        "id": 5,
        "type": "rocket_part",
        "material": {
          "id": 10,
          "type": "material",
          "code": "CS-700",
          "name": "Communication System"
        },
        "quantity": 4
      },
      {
        "id": 4,
        "type": "rocket_part",
        "material": {
          "id": 9,
          "type": "material",
          "code": "NC-600",
          "name": "Navigation Computer"
        },
        "quantity": 5
      }
    ]
  },
  {
    "id": 5,
    "type": "rocket",
    "code": "ARI-6",
    "name": "Ariane 6",
    "parts": [
      {
        "id": 8,
        "type": "rocket_part",
        "material": {
          "id": 18,
          "type": "material",
          "code": "TS-750",
          "name": "Telemetry System"
        },
        "quantity": 1
      },
      {
        "id": 7,
        "type": "rocket_part",
        "material": {
          "id": 17,
          "type": "material",
          "code": "AC-650",
          "name": "Attitude Control System"
        },
        "quantity": 2
      },
      {
        "id": 9,
        "type": "rocket_part",
        "material": {
          "id": 19,
          "type": "material",
          "code": "SM-850",
          "name": "Staging Mechanism"
        },
        "quantity": 3
      },
      {
        "id": 10,
        "type": "rocket_part",
        "material": {
          "id": 20,
          "type": "material",
          "code": "PF-950",
          "name": "Propellant Feed System"
        },
        "quantity": 3
      }
    ]
  }
]

Update rocket parts

To update the list of materials for a specific rocket, we can use the framework automatically generated endpoints. The following request updates the list of materials for the rocket with the ID 1 using the PATCH method:

Update rocket materials using filter
PATCH http://127.0.0.1:8001/rockets?.code=like~*ARI* HTTP/1.1
content-type: application/json

{
  "payload": {
    "parts": [
      {
        "material": 1,
        "quantity": 2
      },
      {
        "material": 4,
        "quantity": 1
      }
    ]
  }
}
Response
{
  "id": 1,
  "type": "rocket",
  "code": "SAT-V",
  "name": "Saturn IB",
  "parts": [
    {
      "id": 11,
      "type": "rocket_part",
      "material": {
        "id": 1,
        "type": "material",
        "code": "ME-1000",
        "name": "Main Engine"
      },
      "quantity": 2
    },
    {
      "id": 12,
      "type": "rocket_part",
      "material": {
        "id": 4,
        "type": "material",
        "code": "GS-200",
        "name": "Guidance System"
      },
      "quantity": 1
    }
  ]
}

Fetch rocket parts

The nested endpoints let us query directly the materials associated with a specific rocket. The following request fetches the materials for the rocket with the ID 1:

Retrieve the updated rocket parts
GET http://127.0.0.1:8001/rockets/1/parts HTTP/1.1
Response
[
  {
    "id": 11,
    "type": "rocket_part",
    "material": {
      "id": 1,
      "type": "material",
      "code": "ME-1000",
      "name": "Main Engine"
    },
    "quantity": 2
  },
  {
    "id": 12,
    "type": "rocket_part",
    "material": {
      "id": 4,
      "type": "material",
      "code": "GS-200",
      "name": "Guidance System"
    },
    "quantity": 1
  }
]

Filter rocket parts

Finally, we can demonstrate the powerfull built-in query capabilities of Plateforme that gives for our RESTful API a GraphQL-like querying for related resources. The following request fetches parts from rocket with id=4 where material codes start with NC, returning only part IDs, quantities, and material details (code and name):

Fetch rocket parts that match specific conditions
GET http://127.0.0.1:8001/rockets/4/parts?.material.code=like~NC* HTTP/1.1
content-type: application/json

{
  "include": {
    "__all__": {
      "id": true,
      "material": {
        "code": true,
        "name": true
      },
      "quantity": true
    }
  }
}
Response
[
  {
    "id": 3,
    "material": {
      "code": "NC-150",
      "name": "Nose Cone"
    },
    "quantity": 2
  },
  {
    "id": 4,
    "material": {
      "code": "NC-600",
      "name": "Navigation Computer"
    },
    "quantity": 5
  }
]

Everything is customizable

Plateforme is designed to be highly customizable, allowing you to tailor the framework to your specific needs. You can extend the built-in functionality by creating custom services, defining additional behaviors, or modifying the default configurations to suit your application requirements.

The built-in route context aware decorator function can be used to define custom routes for your resources. The decorator can be also applied to class or static methods, allowing you to create custom endpoints that perform specific actions at the resource level.

The following example demonstrates how to create a custom route for the Material resource instance as well as a custom route for the Material resource class:

from plateforme import BaseResource, Field
from plateforme.api import AsyncSessionDep, route
from plateforme.database import func, select

class Material(BaseResource):
    code: str = Field(unique=True)
    name: str

    @route.post()  # (1)!
    async def update_name(self, name: str) -> str:
        self.name = name
        return f'Material name updated to {name!r}.'

    @route.get()  # (2)!
    @classmethod
    async def count(cls, session: AsyncSessionDep) -> int:
        query = select(func.count()).select_from(cls)
        result = await session.execute(query)
        return result.scalar()
  1. The update_name method is a custom route defined at the resource instance level. It updates the name attribute of the Material instance and returns a message confirming the update. It can be accessed at the /materials/{_material_key}/update-name endpoint.
  2. The count method is a custom route defined at the resource class level. It queries the database to count the number of Material instances and returns the result. It can be accessed at the /materials/count endpoint.

Use services for reusable logic

While custom routes are useful for defining specific actions at the resource level, services are better suited for encapsulating reusable logic that can be shared across multiple resources. Services can be used to define custom business logic, data validation, or authorization checks, providing a centralized location for common functionality.

The built-in services, such as the CRUDService, can be extended to add additional operations or modify existing ones. You can also create custom services that subscribe to your resources to define specific behaviors or implement complex business logic.

More to come

This is just a basic overview of what Plateforme can do. The documentation will be expanded to cover more advanced features and use cases, so stay tuned for updates!