Sharing Strapi schema with other projects (e.g. front-ends)

I have a functional web app built with Nuxt2 (Vue2) and uses Strapi (v3) in the API server. Up to now, in the front-end I’ve manually built everything: models (not synchronized with the API), components, etc, and I’m starting to build out forms, which ultimately will be very complex and need dynamic validation. In my Strapi instance, I’m quickly approaching +20 collection types, each full of complex fields and relationships. I use REST API for everything, and yes I’m aware of GraphQL but for this project I’m not leveraging it.

Before I go nuts with a ton of manual form work, I want to see if there’s an efficiency I’m overlooking.

I understand the concept of schemas and how they can be shared across different domains (e.g. front-end and back-end) and I’m looking for suggestions here.

As a starting point, how does one get Strapi schema definitions into a front-end client?

1 - Export the Strapi schema into a private NPM module?
Automatically update a private NPM module any time Strapi models are updated, and import this module into any external project. The module is versioned.

2 - Have an API route that responds with the schema and pull it dynamically?
Is this silly? Either at build-time, or in real-time, have an Strapi API route that returns schema; versioned. The return output would be whatever the user is authenticated for, presumably?

3 - Simply copy-paste the schema when you make changes?
If you’re in control over the entire ecosystem (Strapi and multiple front-ends), just copy-paste the schema when the need arises?

4 - Do everything manually, forever?
Basically no automatic synchronization of the content model, and instead every API transaction is purely scrutinized on either side. On server side, if something isn’t right, just return an error.

What do large projects do, especially non-GraphQL ones =P ?

I’ve spent hours searching and I don’t find a lot of material on this.

From my experience the Strapi schemas cannot currently be shared with the frontend. This has the following reasons:

  • Strapi is written in JavaScript and features no static typing
  • Strapi schemas are defined as JSON with a proprietary structure which AFAIK is essentially useless for statically typed clients written in e.g. TypeScript
  • TypeScript support with Strapi itself is not matured enough yet:
    • It is currently an afterthought because the whole architecture and ecosystem is not written with TypeScript in mind
    • The ts:generate-types command generates a huge file which contains very complicated typings and stuff that is irrelevant for the client
    • (I know that improving TS support is in Strapi’s backlog)
  • The flexibility of the REST API is both a blessing and a curse: Because of Population & Field Selection query params the response structure can differ dramatically depending on what you populate and which fields you select
    • The flexibility is similar to using GraphQL with the same benefits and drawbacks

So to answer your question:

4 - Do everything manually, for now

Yes currently it’s this manual work I’m afraid.

1 Like

Thanks for the input; it’s helpful!

You mention Typescript, which I recognize is powerful tech and many devs use it for explicit type management, and it could have benefits in this context, however I think not using it doesn’t stop a schema being shared-and-leveraged in the front-end.

For instance, in generating a form dynamically for a post type (e.g. “event” or “group”), in theory you can easily render fields and validators given a JSON object (being the schema). This doesn’t need Typescript at all. I mean, that is what I’m starting to do now in my front-end with custom models in the global store (VueX). The problem is there’s no relevance (synchronization) to the back-end. When I make changes in the back-end, I have to come and manually modify the models in the front-end… which is an opportunity for mistakes (e.g. typos).

I think if there were some schema export from Strapi, that it would take in the front vs back aspect. That some parts of the schema wouldn’t be exported because it doesn’t need to be, or for security reasons can’t be exposed externally.

It would be awesome if Strapi had a “front-end schema”, such that this is something intended for the front-end to use PROGRAMMATICALLY, in order to properly communicate with the back-end. In hindsight, this is what I desire, and I was crossing my fingers Strapi has this but I’m just overlooking it (due to semantics?).

IMO starting developing a web application that is not statically typed in the year 2023 is a mistake. The key is to catch bugs and inconsistencies before the compilation, before runtime.

It’s a drastic point of view and it does depend on the exact use case, I’m aware of that.

Yes absolutely, your points about validating a JSON schema programmatically are all valid. But when it happens during application runtime, that’s too late. What good does it bring when a JS error or warning is thrown when a validation has failed? This means that I have to actively look for such errors during development by e.g. visiting a page in the browser, following a specific click path or issuing a request to the rest api. If I don’t do that, i see that my code compiles, deploy it and then the validation fails when an end user is using the app.

Hey dude,

So as answer 1 said, if You’re using typescript You can ts generate types
This will create a file “schemas,d.ts” or sth like that
In that file You should have almost all the content types, only that You have to access their “attributes” attribute (so You import the type, but the fields of the content types are like:
import type ApiOrder
type Order = ApiOrder[“attributes”]

What You can do is then add the strapi repo to Your nuxt repo as a submodule, You can then import those types in Your front end code

Hey, in our project Next with Typescript in Frontend we are using the Documentation generated by Strapi and creating our Types with Orval (https://orval.dev/)
Orval then Generates the Fetch Methods and Returntypes for all components, which we then use in our Frontend.
Nice to have : Orval also generates React-Query methods to be used (useGetApiV1CollectionType etc)
I can only Recommend this setup.

Cheers

1 Like

@Andrei_Wilkens Ok, this opened my eyes; thx.

This led me to learn about OpenAPI and Swagger, which I had no idea about and that the Strapi documentation plugin is already spitting this out.

App-side, there seems to be various solutions out there to get Axios to digest an OpenAPI/Swagger doc.

So the plan seems pretty clear, if I leverage all this.

None of this requires Typescript, but for a Typescript project you’d just digest the OpenAPI just the same.

For orval we’re currently on 6.10.3 because 6.11 did some stuff that didnt work with our setup
also We had to remove some stuff from the docs as they were throwing errors while generating.

you can give Orval a transformer method to transform the given OpenAPI specs

Object.keys(a.paths).forEach((path) => {
    [
      '/users',
      '/users-permissions/',
      '/auth/',
      '/connect/',
      '/upload/files',
      'Users-Permissions-PermissionsTree',
    ].forEach((key) => {
      if (path.includes(key)) {
        delete a.paths[path];
      }
    });
  });
  console.log(a.paths['/pages/{id}']);
  a.paths['/pages/{id}'].get.parameters[0].schema.type = 'string';
    a.paths['/pages/{id}'].delete.parameters[0].schema.type = 'string';
  a.paths['/pages/{id}'].put.parameters[0].schema.type = 'string';

  Object.keys(a.components.schemas).forEach((path) => {
    [
      'UploadFile',
      'UploadFolder',
      'UsersPermissions',
      'Users-Permissions',
      'requestBodies',
    ].forEach((key) => {
      if (path.includes(key)) {
        delete a.components.schemas[path];
      }
    });
  });
  // remove some keys because it cannot generate types correctly
    Object.keys(a.components.requestBodies).forEach((path) => {
      [
        'UploadFile',
        'UploadFolder',
        'UsersPermissions',
        'Users-Permissions',
        'Users-Permissions-RoleRequest',
      ].forEach((key) => {
        if (path.includes(key)) {
          delete a.components.requestBodies[path];
        }
      });
    });