Building a REST API With CodeIgniter 4: Structure and Security
How to structure a REST API in CodeIgniter 4: versioned resource routes, a ResourceController, validation, token authentication in a filter, rate limiting, CORS and what to check before launch.

CodeIgniter 4 has what a REST API needs in the core: resource routes, a controller base class that speaks JSON, validation, filters and a throttler. A CodeIgniter 4 REST API that only has to list and save records can be working in an afternoon. The harder part is the structure and the security decisions that are tedious to change once a mobile app or a partner depends on the API.
This walkthrough builds one small resource, orders, from the route to the response, and adds authentication, per-record authorization, rate limiting and CORS in the places the framework intends them to go.
Start with versioned routes
Put the version in the path and in the controller namespace from the first commit. When a breaking change is needed later, version 2 gets its own folder and version 1 keeps running for existing clients.
<?php
// app/Config/Routes.php
$routes->group('api/v1', [
'namespace' => 'App\Controllers\Api\V1',
'filter' => 'tokens',
], static function ($routes): void {
$routes->resource('orders', [
'placeholder' => '(:num)',
'except' => ['new', 'edit'],
]);
});
resource() creates the standard set of routes in one line. The new and edit routes exist to show HTML forms, so an API leaves them out. The (:num) placeholder rejects any ID that is not a number before your code runs.
| Request | Controller method | Purpose |
|---|---|---|
GET /api/v1/orders | index() | List |
GET /api/v1/orders/15 | show($id) | Read one |
POST /api/v1/orders | create() | Create |
PUT or PATCH /api/v1/orders/15 | update($id) | Change |
DELETE /api/v1/orders/15 | delete($id) | Remove |
Auto-routing is off by default in CodeIgniter 4. Leave it off for an API. With every route declared, php spark routes prints the complete list of what is exposed, together with the filters on each route. The user guide chapter on RESTful resource handling lists every option.
A resource controller with one response format
Extend ResourceController, name the model and the format, and implement only the methods you routed.
<?php
namespace App\Controllers\Api\V1;
use App\Models\OrderModel;
use CodeIgniter\RESTful\ResourceController;
class Orders extends ResourceController
{
protected $modelName = OrderModel::class;
protected $format = 'json';
public function index()
{
$orders = $this->model
->where('customer_id', auth()->user()->id)
->paginate(25);
return $this->respond(['data' => $orders]);
}
public function show($id = null)
{
$order = $this->model
->where('customer_id', auth()->user()->id)
->find($id);
return $order === null
? $this->failNotFound()
: $this->respond(['data' => $order]);
}
public function create()
{
$rules = [
'product_id' => 'required|is_natural_no_zero',
'quantity' => 'required|is_natural_no_zero|less_than_equal_to[100]',
];
$input = (array) $this->request->getJSON(true);
if (! $this->validateData($input, $rules)) {
return $this->failValidationErrors($this->validator->getErrors());
}
$data = $this->validator->getValidated();
$data['customer_id'] = auth()->user()->id;
return $this->respondCreated(['id' => $this->model->insert($data)]);
}
}
The response helpers come from the framework's API response trait: respond(), respondCreated(), respondDeleted(), failNotFound(), failValidationErrors(), failForbidden() and others. Using them everywhere gives clients one error shape and the correct status codes without anyone having to remember them.
Decide early what a record looks like from outside. Returning model rows directly exposes every column, including ones added later for internal use. Select the columns you mean to publish, or map each row through a small function, and treat that output as a contract.
Validate every write and save only checked data
In the create() method above, two lines do the security work. validateData() checks the decoded JSON against the rules and collects errors. getValidated() then returns only the fields that had rules. An extra "customer_id": 7 or "status": "paid" in the request body never reaches the model.
The model's $allowedFields property is the second guard, since anything not listed there is dropped on insert and update. Keep both. Fields the client must never set, such as the owner of a record, are assigned on the server from the authenticated user, as the example does.
Authenticate in a filter, authorize per record
Authentication belongs in a filter so that no controller can forget it. The route group above names the tokens filter, which is provided by CodeIgniter Shield, the framework's official authentication library. It reads the Authorization: Bearer header, hashes the token, looks it up and makes the user available through auth()->user(). A request without a valid token gets a 401 before the controller is loaded.
If you write your own filter, follow the same rules: generate tokens from a secure random source, store only a hash, show the plain token to the user once, and support revoking it. Shield also offers HMAC keys and a JWT add-on. For a first-party mobile app or a partner integration, plain revocable access tokens are usually the simpler and safer choice.
Authentication answers who is calling. It does not answer whether they may see order 15. That check is in the controller: every query above includes customer_id, so asking for someone else's order returns a 404 as if it did not exist. Missing ownership checks are the most common serious flaw in APIs, and no filter can add them for you. Token scopes, checked with tokenCan(), are useful for separating read-only clients from those that may write.
Rate limiting with the Throttler
The Throttler class counts actions per key over time. A small filter turns it into a rate limit:
<?php
namespace App\Filters;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
class Throttle implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null)
{
$throttler = service('throttler');
$key = 'api_' . md5($request->getIPAddress());
if ($throttler->check($key, 60, MINUTE) === false) {
return service('response')
->setStatusCode(429)
->setHeader('Retry-After', (string) $throttler->getTokenTime())
->setJSON(['error' => 'Too many requests']);
}
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
}
}
Register it in app/Config/Filters.php and apply it to the API paths:
public array $aliases = [
// ...
'throttle' => \App\Filters\Throttle::class,
];
public array $globals = [
'before' => [
'csrf' => ['except' => 'api/*'],
],
'after' => [],
];
public array $filters = [
'throttle' => ['before' => ['api/*']],
'cors' => ['before' => ['api/*'], 'after' => ['api/*']],
];
Three things to know. The Throttler stores its counters in the cache, so the cache handler must be something other than the dummy handler, and with several web servers it has to be a shared one such as Redis. Filters set here run before route filters, so this limit applies before authentication, which is what you want against token guessing. And a limit per IP address is a blunt tool: add a second, per-token limit for authenticated clients, with a stricter one on the login and token endpoints.
CORS, CSRF and HTTPS
CORS matters only when a browser on another origin calls the API, such as a JavaScript front end on a different domain. Server-to-server clients and native mobile apps ignore it. CodeIgniter ships a cors filter, configured in app/Config/Cors.php:
public array $default = [
'allowedOrigins' => ['https://app.example.com'],
'allowedOriginsPatterns' => [],
'supportsCredentials' => false,
'allowedHeaders' => ['Authorization', 'Content-Type'],
'exposedHeaders' => [],
'allowedMethods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
'maxAge' => 7200,
];
List the exact origins, headers and methods you need. A wildcard origin on an authenticated API is a finding in any review. Browsers send an OPTIONS preflight request first, and filters only run on routes that exist, so add an OPTIONS route for the API paths as the CORS chapter of the user guide shows. CORS is a browser rule and not access control. It never replaces authentication.
CSRF protection is for requests authenticated by cookies. An API that accepts only bearer tokens can be excluded from the csrf filter, as in the configuration above. If a browser front end uses the session cookie to call the API, keep CSRF protection on for those routes. Serve everything over HTTPS and set forceGlobalSecureRequests to true in app/Config/App.php, because a bearer token sent over plain HTTP is a leaked token.
What a CodeIgniter 4 REST API needs before it goes live
CI_ENVIRONMENT = productionin.env, so errors are logged and never returned with stack traces.php spark routesshows a filter on every API route, andphp spark filter:check get api/v1/ordersconfirms the order they run in.- A test with two users proves that neither can read or change the other's records, for every resource.
- Each list endpoint paginates and has a maximum page size.
- Failed logins, rejected tokens and 429 responses are logged with enough detail to spot abuse.
- The API has written documentation, ideally an OpenAPI file that is kept next to the code.
For the wider picture beyond this framework, see API security best practices.
Build it yourself or bring in help
A developer who knows CodeIgniter 4 can build a small internal API from this outline. It is worth involving a team that builds APIs regularly when outside parties will depend on it, when it carries payments or personal data, or when it has to serve a mobile app that cannot be updated on the same day as the server. Those projects need versioning rules, token lifecycles, monitoring and a test suite from the start.
That is the scope of our CodeIgniter API development work, often alongside CodeIgniter 4 development of the application behind it. When the API mainly serves an app, our mobile app API integration team works on both ends.
Frequently asked questions
Does CodeIgniter 4 have built-in API authentication?
The core framework provides filters, which are the place to enforce authentication, but no user system. CodeIgniter Shield is the official add-on for that. It includes access tokens, HMAC keys, session login and route filters for each.
Should a CodeIgniter 4 API use JWT or access tokens?
Access tokens stored as hashes in the database are simpler and can be revoked at once, which suits first-party apps and partner integrations. JWTs avoid a database lookup per request and fit setups with several services, at the cost of harder revocation. Choose JWT only when you need what it offers.
Can the same CodeIgniter application serve web pages and an API?
Yes. Keep the API controllers in their own namespace and route group, with token authentication and no CSRF filter, while the web pages use session login and CSRF protection. Both can share the same models and validation rules.
How do I add a new API version without breaking clients?
Create a new controller namespace and a new route group, such as api/v2, and change behavior only there. Adding optional fields to version 1 is safe. Removing or renaming fields, or changing their meaning, belongs in the new version, with a published end date for the old one.
Add worldwincoder.com as a preferred source on Google, or open this article in your AI assistant to use it as a source.
