How to build a JSON REST API with CodeIgniter 4.7
A step-by-step CodeIgniter 4.7 tutorial: create a books API with a migration, a validating model, the new API transformers, resource routes and paginated JSON responses, then test every endpoint with curl.
Updated 9 min read

In this article
Most CodeIgniter projects eventually need a JSON API: a mobile app wants the same data as the website, a JavaScript front end needs endpoints, or a partner asks for an integration. CodeIgniter 4 has had resource routes and a ResourceController for years, but version 4.7 added two pieces that remove a lot of hand-written plumbing: a paginate() helper on the API response trait and API transformers that control exactly which fields leave your server.
This tutorial builds a small, complete books API on CodeIgniter 4.7.4, the current release at the time of writing (published on 7 July 2026). You will create the table with a migration, add a model with validation, shape the output with a transformer, wire up resource routes and test every endpoint with curl. The result is a reasonable starting point for a real project, and each step links back to the official user guide page it is based on.
The examples assume you are comfortable with PHP and Composer. They do not cover authentication; a companion article on Cloudsoft Zone shows how to protect these same endpoints with CodeIgniter Shield access tokens.
What you need before you start
CodeIgniter 4.7 raised the minimum PHP version. Check your environment against the official server requirements before you install anything:
- PHP 8.2 or newer, with the
intlandmbstringextensions enabled. PHP 8.5 needs CodeIgniter 4.7.0 or later. - Composer 2.0.14 or later, which the CodeIgniter installation guide lists as a requirement.
- A database. This tutorial uses MySQL through the MySQLi driver (enable the
mysqlndextension). SQLite3, PostgreSQL, SQL Server and Oracle are also supported.
The user guide notes that the end-of-life date for PHP 8.2 is 31 December 2026, so if you are setting up a new server today, choose PHP 8.3 or newer.

Step 1: Create the project and configure the database
Create a new application from the official app starter and copy the sample environment file:
composer create-project codeigniter4/appstarter books-api
cd books-api
cp env .env
php spark env developmentphp spark env development writes CI_ENVIRONMENT = development into .env, which turns on detailed error pages while you work. Never deploy with this setting: CodeIgniter runs in production mode by default, and that is what your live server should use.
Next, open .env, uncomment the database section and point it at an empty database you have created:
database.default.hostname = 127.0.0.1
database.default.database = books_api
database.default.username = books_user
database.default.password = change-me
database.default.DBDriver = MySQLi
database.default.port = 3306Keep real credentials in .env only. The app starter's .gitignore already excludes it, so it will not end up in your repository.
Step 2: Create the table with a migration
Migrations keep your schema in version control. Generate a skeleton with Spark:
php spark make:migration CreateBooksSpark creates a timestamped file in app/Database/Migrations/. Replace its contents with the following:
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateBooks extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 200,
],
'author' => [
'type' => 'VARCHAR',
'constraint' => 120,
],
'published_year' => [
'type' => 'SMALLINT',
'unsigned' => true,
'null' => true,
],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->createTable('books');
}
public function down()
{
$this->forge->dropTable('books');
}
}Run it with php spark migrate and confirm with php spark migrate:status. If several servers might run migrations at the same time during a deployment, CodeIgniter 4.7 can take a lock first: set Config\Migrations::$lock = true.
Step 3: Add a model with validation
The model is where input rules live, so every controller that saves a book gets the same checks. Create app/Models/BookModel.php:
<?php
namespace App\Models;
use CodeIgniter\Model;
class BookModel extends Model
{
protected $table = 'books';
protected $primaryKey = 'id';
protected $returnType = 'array';
protected $allowedFields = ['title', 'author', 'published_year'];
protected $useTimestamps = true;
protected $dateFormat = 'datetime';
protected $validationRules = [
'title' => 'required|max_length[200]',
'author' => 'required|max_length[120]',
'published_year' => 'permit_empty|integer|greater_than[1450]|less_than_equal_to[2100]',
];
}Three settings do most of the security work here:
$allowedFieldsis an allowlist. Any other key in the request body, such asidorcreated_at, is ignored when saving. The CodeIgniter security guidelines point to this property as the framework's answer to mass-assignment problems (OWASP API3:2023).$validationRulesrun automatically oninsert(),update()andsave(). If validation fails, the method returnsfalseand$model->errors()holds the messages.$cleanValidationRulesistrueby default, which means that on an update, rules for fields that are not in the submitted data are removed temporarily. That is what lets aPATCHrequest change only the title without failing therequiredrule on the author.
Step 4: Shape the output with a transformer
Returning raw database rows couples your API to your table design. A transformer, new in 4.7, sits between the two. Generate one:
php spark make:transformer Book --suffixThe --suffix option creates app/Transformers/BookTransformer.php. Fill in toArray():
<?php
namespace App\Transformers;
use CodeIgniter\API\BaseTransformer;
class BookTransformer extends BaseTransformer
{
public function toArray(mixed $resource): array
{
return [
'id' => (int) $resource['id'],
'title' => $resource['title'],
'author' => $resource['author'],
'year' => $resource['published_year'] === null ? null : (int) $resource['published_year'],
'createdAt' => $resource['created_at'],
];
}
protected function getAllowedFields(): ?array
{
return ['id', 'title', 'author', 'year', 'createdAt'];
}
}Transformers support a fields query parameter out of the box, so a client can request /api/books/1?fields=id,title to receive a smaller payload. Overriding getAllowedFields() limits which names a client may ask for; a request for anything else raises an ApiException. Version 4.7.4 fixed a bug where the root request's fields and include parameters leaked into nested transformers, which is one more reason to be on the latest patch release.
Step 5: Write the resource controller
ResourceController already uses ResponseTrait, so methods such as respond(), respondCreated(), failNotFound() and the new paginate() are available. Create app/Controllers/Api/Books.php:
<?php
namespace App\Controllers\Api;
use App\Models\BookModel;
use App\Transformers\BookTransformer;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\RESTful\ResourceController;
class Books extends ResourceController
{
protected $modelName = BookModel::class;
protected $format = 'json';
// GET /api/books?page=2
public function index()
{
return $this->paginate(
resource: $this->model->orderBy('id', 'DESC'),
perPage: 20,
transformWith: BookTransformer::class,
);
}
// GET /api/books/{id}
public function show($id = null)
{
$book = $this->model->find($id);
if ($book === null) {
return $this->failNotFound('Book not found.');
}
return $this->respond((new BookTransformer())->transform($book));
}
// POST /api/books
public function create()
{
$data = $this->jsonBody();
if ($data === null) {
return $this->fail('Send a JSON object in the request body.', 400);
}
$id = $this->model->insert($data);
if ($id === false) {
return $this->failValidationErrors($this->model->errors());
}
$book = $this->model->find($id);
return $this->respondCreated((new BookTransformer())->transform($book));
}
// PUT or PATCH /api/books/{id}
public function update($id = null)
{
if ($this->model->find($id) === null) {
return $this->failNotFound('Book not found.');
}
$data = $this->jsonBody();
if ($data === null || $data === []) {
return $this->fail('Send a non-empty JSON object in the request body.', 400);
}
if ($this->model->update($id, $data) === false) {
return $this->failValidationErrors($this->model->errors());
}
return $this->respond((new BookTransformer())->transform($this->model->find($id)));
}
// DELETE /api/books/{id}
public function delete($id = null)
{
if ($this->model->find($id) === null) {
return $this->failNotFound('Book not found.');
}
$this->model->delete($id);
return $this->respondDeleted(['id' => (int) $id]);
}
/**
* Decodes the JSON body; returns null for a missing, malformed or non-object body.
*/
private function jsonBody(): ?array
{
try {
$data = $this->request->getJSON(true);
} catch (HTTPException) {
return null;
}
return is_array($data) ? $data : null;
}
}A few details are worth noticing. getJSON(true) decodes the body into an array and throws an HTTPException when the JSON is malformed, so the small jsonBody() helper turns both a malformed and a non-object body into a clean 400 response. Each write method checks that the record exists before touching it, so clients receive a clear 404 instead of a silent success. And because CodeIgniter 4.7 validates primary keys before running queries, values such as 0 or an empty string now throw an InvalidArgumentException rather than a database error. The (:num) route placeholder in the next step keeps non-numeric IDs out entirely.

Step 6: Register resource routes
Open app/Config/Routes.php and add a route group for the API:
$routes->group('api', ['namespace' => 'App\Controllers\Api'], static function ($routes) {
$routes->resource('books', [
'placeholder' => '(:num)',
'except' => 'new,edit',
]);
});resource() generates the standard REST routes. except drops the new and edit routes, which return HTML forms and have no place in a JSON API. Run php spark routes to see the final table. You should get:
| Method | Path | Controller method | Success status |
|---|---|---|---|
| GET | /api/books | index() | 200 |
| GET | /api/books/(:num) | show($id) | 200 |
| POST | /api/books | create() | 201 |
| PUT / PATCH | /api/books/(:num) | update($id) | 200 |
| DELETE | /api/books/(:num) | delete($id) | 200 |
The routing guide warns that routes match in the order you define them. If you later add something like books/search, define it above the resource() call so the show route does not capture it first.
Step 7: Test the endpoints
Start the development server with php spark serve (it listens on port 8080 by default) and try the API:
# Create a book
curl -s -X POST http://localhost:8080/api/books \
-H 'Content-Type: application/json' \
-d '{"title":"The Pragmatic Programmer","author":"David Thomas, Andrew Hunt","published_year":2019}'
# List books (paginated)
curl -s 'http://localhost:8080/api/books?page=1'
# Fetch only some fields
curl -s 'http://localhost:8080/api/books/1?fields=id,title'
# Partial update
curl -s -X PATCH http://localhost:8080/api/books/1 \
-H 'Content-Type: application/json' -d '{"published_year":2020}'
# Validation failure (missing author)
curl -s -X POST http://localhost:8080/api/books \
-H 'Content-Type: application/json' -d '{"title":"No author"}'The list endpoint wraps results in a data array and adds meta (page, perPage, total, totalPages) and links (self, first, last, next, previous) elements. The validation failure returns status 400 with a body containing status, code and a messages object keyed by field name, which is the standard shape produced by fail().
Common problems and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| POST returns 403 or a CSRF error | The csrf filter is enabled globally | Exclude the API paths, for example 'csrf' => ['except' => ['api/*']], and protect the API with tokens instead |
404 for /api/books/abc | The (:num) placeholder rejects non-numeric IDs | Expected behaviour; clients should send numeric IDs |
| Fields you sent are silently ignored | They are not in $allowedFields | Add them deliberately, never by default |
| Response is XML instead of JSON | Content negotiation picked XML from the Accept header | Keep protected $format = 'json' on the controller |
Old regex_match rules stop working after upgrading | 4.7 requires double braces for placeholders | Change {placeholder} to {{placeholder}} |
Next steps
You now have a working CRUD API with validation, pagination and controlled output. Before it goes anywhere near production, add authentication (Shield's access tokens or JWT authenticator), apply rate limiting with the Throttler or Shield's auth-rates filter on login routes, and work through a deployment checklist: CI_ENVIRONMENT set to production, composer install --no-dev, HTTPS enforced and the document root pointing at public/. Write feature tests for each endpoint as you go, so the behaviour above stays the behaviour you ship.
Frequently asked questions
Which PHP version does CodeIgniter 4.7 need?
PHP 8.2 or newer, with the
intlandmbstringextensions. PHP 8.5 is supported from CodeIgniter 4.7.0.Should I use ResourceController or a normal controller with ResponseTrait?
Either works.
ResourceControlleralready includesResponseTraitand matches the method names that$routes->resource()expects, so it saves some wiring for standard CRUD. For endpoints that do not fit the CRUD pattern, a normal controller that usesResponseTraitis simpler.What is an API transformer in CodeIgniter?
A class extending
CodeIgniter\API\BaseTransformer, added in 4.7, that converts an entity, array or object into the exact structure your API returns. It also supports thefieldsandincludequery parameters.How do I paginate API results?
Call
$this->paginate($modelOrBuilder, $perPage, TransformerClass::class)from a controller usingResponseTrait. It reads the page number from the query string and returnsdata,metaandlinks.
Your next customer could start right here.
Introduce your tool, product or business to readers exploring technology.
Explore placements and request a private quote
Sources
- Server Requirements — CodeIgniter Foundation
- RESTful Resource Handling — CodeIgniter Foundation
- API Responses — CodeIgniter Foundation
- API Resources (Transformers) — CodeIgniter Foundation
- Using CodeIgniter's Model — CodeIgniter Foundation
- Database Migrations — CodeIgniter Foundation
- Version 4.7.4 change log — CodeIgniter Foundation, 2026-07-07
- Composer Installation — CodeIgniter Foundation
Written with substantial AI assistance. Facts checked against cited sources. Editorial policy.
Comments
No comments yet. Be the first to join the conversation.
Leave a comment
Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.
Related tools
- Directory profile
GitHub
Git-based platform for hosting code, reviewing pull requests, tracking issues and running CI/CD workflows, with free and per-user paid plans.
A tool connected to a service selected for this article. Compare its fit for your project; inclusion is not an endorsement.
- Directory profile
Postman
API platform for designing, testing, documenting and monitoring APIs, with desktop and web apps, a CLI and a free plan.
A tool connected to a service selected for this article. Compare its fit for your project; inclusion is not an endorsement.
More from Cloudsoft Zone

Troubleshooting guideCodeIgniter
CodeIgniter 4 validation not working? 9 causes and fixes
Fix CodeIgniter 4 validation problems fast: wrong input source, lost errors after redirect, permit_empty, strict JSON rules, is_unique placeholders and more.
By Cloudsoft Zone

Industry updateCodeIgniter
CodeIgniter 4.7.5 security release: what to patch and how
CodeIgniter 4.7.5 fixes six advisories, including a critical upload bypass and Parser code execution. See who is exposed and how to upgrade safely.
By Cloudsoft Zone

Laravel
Upgrading from Laravel 12 to Laravel 13: a practical guide
Laravel 12 lost bug-fix support in August 2026. This guide covers the PHP 8.3 requirement, Composer changes, the CSRF middleware rename, cache and session hardening, low-impact changes that often bite, and a safe rollout plan.
By Cloudsoft Zone
