Skip to main content

Cloudsoft Zone Build, deploy and scale with practical guides to PHP, Node.js, React, DevOps and AI automation. Explore the guides

Cloudsoft Zone
CodeIgniterDevelopmentPHPTutorial

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.

Cloudsoft Zone

Updated 9 min read

Code editor with a dark theme showing PHP source code that builds HTML responses
Photograph: Yuri Samoilov / CC BY 2.0, via Wikimedia Commons
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 intl and mbstring extensions 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 mysqlnd extension). 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.

CodeIgniter logo with an orange flame symbol beside the CodeIgniter wordmark
CodeIgniter 4.7 adds API transformers and a paginate() helper for JSON APIs.

Step 1: Create the project and configure the database

Create a new application from the official app starter and copy the sample environment file:

Shell
composer create-project codeigniter4/appstarter books-api
cd books-api
cp env .env
php spark env development

php 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:

INI
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     = 3306

Keep 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:

Shell
php spark make:migration CreateBooks

Spark creates a timestamped file in app/Database/Migrations/. Replace its contents with the following:

PHP
<?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
<?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:

  • $allowedFields is an allowlist. Any other key in the request body, such as id or created_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).
  • $validationRules run automatically on insert(), update() and save(). If validation fails, the method returns false and $model->errors() holds the messages.
  • $cleanValidationRules is true by default, which means that on an update, rules for fields that are not in the submitted data are removed temporarily. That is what lets a PATCH request change only the title without failing the required rule 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:

Shell
php spark make:transformer Book --suffix

The --suffix option creates app/Transformers/BookTransformer.php. Fill in toArray():

PHP
<?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
<?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.

Close-up of a monitor showing dense, colour-highlighted source code with a shallow depth of field
Keeping validation in the model and output shaping in a transformer keeps each controller method short.

Step 6: Register resource routes

Open app/Config/Routes.php and add a route group for the API:

PHP
$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:

MethodPathController methodSuccess status
GET/api/booksindex()200
GET/api/books/(:num)show($id)200
POST/api/bookscreate()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:

Shell
# 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

SymptomLikely causeFix
POST returns 403 or a CSRF errorThe csrf filter is enabled globallyExclude the API paths, for example 'csrf' => ['except' => ['api/*']], and protect the API with tokens instead
404 for /api/books/abcThe (:num) placeholder rejects non-numeric IDsExpected behaviour; clients should send numeric IDs
Fields you sent are silently ignoredThey are not in $allowedFieldsAdd them deliberately, never by default
Response is XML instead of JSONContent negotiation picked XML from the Accept headerKeep protected $format = 'json' on the controller
Old regex_match rules stop working after upgrading4.7 requires double braces for placeholdersChange {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 intl and mbstring extensions. PHP 8.5 is supported from CodeIgniter 4.7.0.

  • Should I use ResourceController or a normal controller with ResponseTrait?

    Either works. ResourceController already includes ResponseTrait and 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 uses ResponseTrait is 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 the fields and include query parameters.

  • How do I paginate API results?

    Call $this->paginate($modelOrBuilder, $perPage, TransformerClass::class) from a controller using ResponseTrait. It reads the page number from the query string and returns data, meta and links.

Advertise on CloudSoftZone

Your next customer could start right here.

Introduce your tool, product or business to readers exploring technology.

Advertise with us

Explore placements and request a private quote

Sources

  1. Server Requirements — CodeIgniter Foundation
  2. RESTful Resource Handling — CodeIgniter Foundation
  3. API Responses — CodeIgniter Foundation
  4. API Resources (Transformers) — CodeIgniter Foundation
  5. Using CodeIgniter's Model — CodeIgniter Foundation
  6. Database Migrations — CodeIgniter Foundation
  7. Version 4.7.4 change log — CodeIgniter Foundation, 2026-07-07
  8. 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.

Never published. Used only to contact you about this comment.

Loading the security check…

Comments are moderated before publication.

  • Plush elePHPant toy from Heroku, a version of the PHP community mascot, photographed close up

    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