In previous articles in this series we have gone over the schema, authentication, UI and managing the project. For this article we'll look at the API.
What is an API?
API stands for Application Programming Interface and is a way to access an application without using the user interface. These are usually used for 1 program to talk to another program without having to understand anything about the architecture or language. There are many types of API including OpenGL or DirectX, but we are looking specifically at a Web API to access the CMS webapp. As this is a webapp we have many API architectures we can choose from including SOAP and REST APIs. We won't go into everything API here and if you are interested and want to more about them, you could check out the Wiki page.
Our API is a REST API, but before we look at REST, let's quickly look at why we need an API.
Why do we need an API?
We are building a Content Management System which is a way to manage content, why do we need an API? The simple answer is, the API is the method for accessing the content from the data store. Without an API, there would be no way for anyone to access the content.
Let's go with the example of a blog page. If we go to a link like /articles/my-cat-bob, we would expect to see the article about 'My cat Bob'. However, all the content is stored in a data store and the data store does not know what 'My cat Bob' is, it wont even know what /articles/my-cat-bob is, so we have a API that knows how to find the article in the data store and send it back to the user. It will look for the type:article and the slug:my-cat-bob from data store using the URL we entered.
Notice the URL is articles and the type is article? In this app, data is stored as singular, but RESTful URL resources are always plural. We can't simply add an 's' to the end either, we have an apiResourceMap which maps the storage name (table/collection) to the API resource.
Using the example above, the article might have a few versions, the API will get the 'active' one by understanding the relationships with other resources. Using relationships the API can get the Author and the date the article was last updated and maybe all other related articles. See more about this in Query Strings below
Now, let's take our example a bit further and use products, not articles. We have stock and price and different currencies and everything else that comes with a product. Our currency might be stored in a different system, and our stock might be a different system again, but the API should be able to get all of this information, bring it together and pass it to the consumer of the API without the consumer knowing the any of this.
To answer the question, we need an API so a consumer can access the data and the relationships of the data without knowing anything about the underlying data.
We know why we need the API, let's look at the architecture type we have chosen.
RESTful
Our API will be RESTful, which means we will have a bunch of resources like users or roles that we will access using URLs like /api/users or /api/roles. In a production app, the URLs would be fully formed so we would use something like https://www.huytonweb.com/api/users, but as this CMS is to be installed on any server, the domain will change, so we'll stick with examples like /api/users for this article.
The first decision we must make is plural or singular for the resources. This is something that becomes a nightmare later if it is not decided at the beginning. For most RESTful APIs plural is best so we will choose this (I won't go into why here, but this is a RULE that, if broken by a developer, will break the API), so we have /api/users never /api/user.
Now we have our resources, we can look at how we get different data from the same resource or change that data. If we go to /api/users we will get all users on the system, if we go to /api/users/123 we will get the user with the id of 123.
Both of these are using HTTP GET requests, RESTful deals with the HTTP methods:
GET- Get a resourcePOST- Create a new resourcePUT- Update ALL attributes of a resourcePATCH- Update some attributes of a resourceDELETE- Remove a resource
These match up well with the usual data CRUD (Create, Read, Update & Delete) that we would usually use for data. For example, when we Create a new User we POST to /api/users with the payload of the new user in the body like:
{
"data": {
"attributes": {
"name": "Mr Test",
"email": "test@test.com"
}
}From this we would receive an response from the server with a status of 201 (created), if you don't know your HTTP response codes have a look at HTTP Cat and you can see 201 is Created.
In the response there is also a body containing data like:
{
"data": {
"id": "123",
"attributes": {
"name": "Mr Test",
"email": "test@test.com",
"email_verified": null,
"image": null,
"password_hash": null,
"password_reset_token": "1ca65c363c21b3909b57f6c9a908e7e4d753317cfdff2c4799a85ddd049d42e8",
"password_reset_token_expiry": "2026-01-10T10:44:58.917Z",
"deleted_at": null,
"created_at": "2026-01-09T10:44:58.92Z",
"updated_at": "2026-01-09T10:44:58.92Z"
},
}This tells the API consumer their new user has been created with the id of 123 and all the basic information about that user.
If we take the same URL, and the same payload, but we send it as a PATCH we will update the data in the database, so if we want to change the name to 'Mr Test Test' we simply PATCH with the new name. If we send it as a DELETE, then we'll remove it from the database.
PUT is a little different, with a PUT we must update ALL attributes or anything is automatically null that is not sent. For this reason we don't use PUT too much as it can be dangerous (think of changing a user status in a PUT, but not passing the username back and it becomes null), however, to be fully RESTful, we need to have ALL of the HTTP methods available.
We have also added an index page to our API so if a consumer has access, they can see all the resources available and the links to get to them making the API automatically documenting.
This is a good, solid start to the API, but if we carry on this way we will need full API documentation and any 3rd party that wants to connect to our API will need to be 'tweaked' and data 'transformed' so it works for every consumer.
Instead, what we need is a known specification that already has the documentation and that can connect to 3rd parties out of the box. For this, we have chosen JSON:API 1.1, but before we get into spec, let's have a look at the tools we use.
Tools
To develop a RESTful API we need to see what the API does as we go along. A browser will do GET requests when there are no security gates (that's what you are doing when you go to any URL), but we have already created our authentication and security gates, plus we need to look at the other 4 HTTP methods too.
There are a few REST clients like Nightingale or POSTMAN. We like Nightingale at the moment as it's simple and not full of extra functionality we don't really need.
As we have our CMS app and we are using Next.js, there really isn't any other tools we need to develop the API, so let's get onto the specification.
JSON:API 1.1
We have an API and it's pretty good. It is fully RESTful and follows best practice from blogs like the Clean Coder (Uncle "Bob" Martin) and books like Build APIs you wont hate by Phil Sturgeon. However, 3rd parties can't simply connect to an API like this, we need more 'rules', we need a full specification we can follow. Enter JSON:API 1.1.
Request
Let's look at our payload from the example above and add in the JSON:API 1.1
{
"data": {
"type": "users",
"attributes": {
"name": "Mr Test",
"email": "test@test.com"
}
}We've now added the type, which mirrors the resource in the URL. This is a duplication here, but allows an API consumer to update the users without having to see the URL. Everything is in the body of the API request.
Our new JSON:API response will look something like this:
{
"jsonapi": {
"version": "1.1"
},
"data": {
"id": "123",
"type": "users",
"attributes": {
"name": "Mr Test",
"email": "test@test.com",
"email_verified": null,
"image": null,
"password_hash": null,
"password_reset_token": "1ca65c363c21b3909b57f6c9a908e7e4d753317cfdff2c4799a85ddd049d42e8",
"password_reset_token_expiry": "2026-01-10T10:44:58.917Z",
"deleted_at": null,
"created_at": "2026-01-09T10:44:58.92Z",
"updated_at": "2026-01-09T10:44:58.92Z"
},
"relationships": {
"accounts": {
"links": {
"self": "/api/users/123/relationships/accounts",
"related": "/api/users/123/accounts"
},
"meta": {
"relation-type": "to-many"
},
"data": []
},
"sessions": {
"links": {
"self": "/api/users/123/relationships/sessions",
"related": "/api/users/123/sessions"
},
"meta": {
"relation-type": "to-many"
},
"data": []
},
"roles": {
"links": {
"self": "/api/users/123/relationships/roles",
"related": "/api/users/123/roles"
},
"meta": {
"relation-type": "to-many"
},
"data": [
{
"type": "roles",
"id": "cmk5pcibi001io1h0wr0xve1y"
}
]
},
"content_revisions": {
"links": {
"self": "/api/users/123/relationships/content_revisions",
"related": "/api/users/123/content_revisions"
},
"meta": {
"relation-type": "to-many"
},
"data": []
}
},
"links": {
"self": "/api/users/123"
}
},
"meta": {
"request-id": "213d5fc4-53b3-4fc7-9af0-6e5ae06c839b",
"retrieved-at": "2026-01-09T10:44:58.929Z"
},
}This looks much larger than the one we had previously and looks like we have a mass of unneeded information, but it is actually human readable and logical. Let's go through it.
Version
We now have the spec and the version of the spec so anything that can read this spec knows how everything is formatted:
"jsonapi": {
"version": "1.1"
},Data & Attributes
Next we have our data and attributes for this user which are the same as before, so it's the same data from the database. The addition here is the type users which we know about from above, the relationships (which I'll go into in more detail below) and the links of which we have self at the moment which links back to the resource (we may add more here as the API matures).
"data": {
"id": "123",
"type": "users",
"attributes": {
"name": "Mr Test",
"email": "test@test.com",
"email_verified": null,
"image": null,
"password_hash": null,
"password_reset_token": "1ca65c363c21b3909b57f6c9a908e7e4d753317cfdff2c4799a85ddd049d42e8",
"password_reset_token_expiry": "2026-01-10T10:44:58.917Z",
"deleted_at": null,
"created_at": "2026-01-09T10:44:58.92Z",
"updated_at": "2026-01-09T10:44:58.92Z"
},
"relationships": { ... },
"links": {
"self": "/api/users/123"
}Meta
Lastly, we have the meta. In this case we have the request id which is used for logging e.g. if a user has an issue they can provide the request id and we can search the logs for it. We also have a timestamp, also great for logging.
"meta": {
"request-id": "213d5fc4-53b3-4fc7-9af0-6e5ae06c839b",
"retrieved-at": "2026-01-09T10:44:58.929Z"
},It's worth noting if we have a lot of resources, for example logs. instead of just getting all the logs with:
/api/logs
we can do something like this:
/api/logs?page[number]=1&page[size]=10
This says page number 1 and 10 logs per page. With something like this, our meta looks a little different and we also have a new links object added to the root so we get something like:
"meta": {
"request-id": "5dfe8022-53fc-4e50-b4f1-755350bdb1e9",
"retrieved-at": "2026-01-09T15:33:47.47Z",
"total-count": 134,
"count": 10,
"page": 1,
"page-size": 10,
"total-pages": 14
},
"links": {
"self": "/api/logs?page[number]=1&page[size]=10",
"first": "/api/logs?page[number]=1&page[size]=10",
"last": "/api/logs?page[number]=14&page[size]=10",
"next": "/api/logs?page[number]=2&page[size]=10",
"prev": null
}From this we can see there are 134 logs, we are on page 1, we have 10 per page and 14 pages (the last page would have a count of 4, not 10).
We also have all the links to the pagination we need within the links too.
The maths gets quite complex for the pagination on the API, but once complete, it saves the API consumers having to write the same pagination over and over, thus preventing duplication of effort.
Relationships
In the above example we have relationships and inside we have:
- accounts
- sessions
- roles
- content_revisions
These are all related data we could get with our user with the id 123. Let's look at 1 of these as all relationships work the same way:
"roles": {
"links": {
"self": "/api/users/123/relationships/roles",
"related": "/api/users/123/roles"
},
"meta": {
"relation-type": "to-many"
},
"data": [
{
"type": "roles",
"id": "cmk5pcibi001io1h0wr0xve1y"
}
]
},We can see there is a relationship to the type roles and we can see links on how to find more about this relationship.
We also know this is a 'to-many' relationship i.e. many users can have many roles e.g. user 123 has the role "id": "cmk5pcibi001io1h0wr0xve1y" (I know this is the User Role id at the moment), but this user could have many roles like Editor or Admin or any number of roles.
Looking at this, we cannot see the Role name or when it was created or anything about it. Now, we could get the data for the role with /api/roles/cmk5pcibi001io1h0wr0xve1y which is fine, but it is an extra hit on the API and possibly the database, so, instead, we can use include, like this:
/api/user/123?include=roles
What this does is include the role with the id cmk5pcibi001io1h0wr0xve1y in an object at the bottom of the user object, with all the attributes of the role. You've probably guessed why this is pretty clever, it means we only need the id of the related object as a reference and if we have 200 users, we don't duplicate the same data for the User role 200 times with every other role. That data is added once at the bottom for each role.
If we chose not to include the roles (as we have here) then we still have the id so we can link back to it later.
The relationships of the API is very powerful and allows an API consumer to find out all of the data from a single resource instead of multiple hits to the API.
Query String
The query string is anything after the question mark, like the pagination and include above. There are a bunch more attributes we have added to the API that can be used in the query string so the consumer to have flexibility when using the API. These are:
- page[number] - The page we want (default 1)
- page[size] - The number of items per page (default 10)
- sort - Sort order minus (-) denotes descending
- include - If we want to include any relationship data
- q - For search
- fields[<resource-type>]=<field>,<field> - This allows us to choose fields to show for any resource.
- filter[<field>] - We can filter fields like [<field>] where the field is or isn’t something with [eq] for equal or [ne] for not equal, so filter
[deleted_at][eq]=nullfor all that ‘deleted_at’ are null - filter[<field>][in]=<value>,<value> - filter with a number of parameters (up to 10) should use
inthen a comma separated list e.g.filter[level][in]=warn,error
All of these are probably self exoplanetary, except maybe include. Include is part of the relationships and explained above.
We've added the JSON:API 1.1 spec to the API and it was a very large piece of work. Should we have left the API not to spec and have it 'bespoke'?
Is a spec good or bad?
As you can see, using JSON:API 1.1 is much more complex than the basic RESTful API, but the spec is so much more flexible.
Any good RESTful API must have all the functionality outlined in the specification, so we would have built in as the API matured, but we would have needed lots of documentation and may have missed useful functionality or duplication or many, many other problems. Things like the relationships and pagination handled in this way is difficult to understand why at first, but is powerful when added to nested includes like user -> role -> permissions.
In a system that we are building MVP, starting with an API to full specification seems wrong, we might expect the basic functionality then add as we go. However, without the full relationships and includes, getting data would need multiple hits on the database and API which is an extra cost and would mean refactoring every app that uses the API as we go.
For this CMS, it came down to a simple fact, making the API to spec now will save time later.
Adding this spec may be too much for some, but having built many APIs, I don't think I will ever build an API that is not to this spec again.
We now understand the API and how it works, but how do we stop consumers doing things they shouldn't?
Security
The Admin side of the CMS and the presentation side of the CMS will use the API to access the data from the data store, no part of the system will to the data store. This means the API is not just an addon, it is integral to the system.
We have built a Domain Manager so the system admin can allow 3rd parties to access the CMS through the API and using the Roles and Permissions the admin can allow some domains certain access and other domains other access, for example there may be a 10 or 20 of apps that should only read content, so a Role called Content reader is added, with only the permission to read content and categories, tags, menus and authors. There may be 2 apps that need to create content, so a content manager role is created and the 2 apps are given that role.
The Domain Manager with Roles & Permissions is extremely powerful, it is flexible and allows security of the API for any number of consumers, right from the admin side.
Finishing up
We now have a system with a flexible data storage pattern, a powerful and flexible API that sits above it, a 3 gated security (HMAC, API Key & Session). In our next article we come to what this system is built for, the content and how to manage it.
If I've missed anything or you need any web help, as usual, leave a comment or contact us. We're looking for clients big or small, so if you have an idea, get in touch and we'll do our best to help.
