Milk Admin

API

Revision: 2025-11-11

RESTful API system with JWT authentication, automatic routing, and endpoint documentation.

Response Format

All API responses follow a standardized JSON format:

Success responses:

{
    "success": true,
    "data": { /* your data */ }
}

Error responses:

{
    "success": false,
    "message": "Error description"
}

Endpoint Registration

In modules (extends AbstractApi or extended AbstactModule):

class MyTestApi extends AbstractApi {
    #[ApiEndpoint('my-test/hello')]
    public function apiHello($request) {
        return $this->success(['message' => 'Hello World']);
    }
}

With authentication and HTTP method:

class UsersApi extends AbstractApi {
    #[ApiEndpoint('users/create', 'POST', ['auth' => true])]
    public function apiCreate($request) {
        $data = $request['body'];
        return $this->success(['id' => 1, 'username' => $data['username']]);
    }
}

Manual registration:

use App\API;

API::set('test/hello', function($request) {
    return ['message' => 'Hello World'];
});

Documenting APIs

Add documentation using the #[ApiDoc] attribute:

use App\Attributes\{ApiEndpoint, ApiDoc};

class PostsApi extends AbstractApi {
    #[ApiEndpoint('posts/create', 'POST', ['auth' => true])]
    #[ApiDoc(
        'Create a new blog post',
        ['body' => ['title' => 'string', 'content' => 'string']],
        ['id' => 'int', 'title' => 'string', 'created_at' => 'datetime']
    )]
    public function createPost($request) {
        return $this->success(['id' => 1, 'title' => 'New Post']);
    }
}

With API::set():

API::set('test/hello',
    function($request) {
        return ['message' => 'Hello World'];
    },
    [],                                          // Options
    'Returns a hello world message',             // Description
    [],                                          // Parameters
    ['message' => 'string']                      // Response structure
);

Exception Handling

Throw exceptions in your API handlers - the framework catches and formats them automatically:

use App\Exceptions\{ApiException, ApiAuthException};

API::set('users/delete', function($request) {
    $id = $request['input']('id');

    if (!$id) {
        throw new ApiException("User ID required", 422);
    }

    $user = User::find($id);
    if (!$user) {
        throw new ApiException("User not found", 404);
    }

    if ($user->id !== API::user()->id) {
        throw new ApiAuthException("Cannot delete other users", 403);
    }

    $user->delete();
    return ['success' => true];
}, ['auth' => true, 'method' => 'DELETE']);

Available exceptions:

  • ApiException - General API errors (default 400)
  • ApiAuthException - Authentication/authorization errors (default 403)

Methods

API::set(string $page, callable|string $handler, array $options = [], ?string $description = null, ?array $parameters = null, ?array $response = null) : void

Registers an API endpoint.

API::set('users/list', function($request) {
    return ['users' => User::all()];
});

API::group(array $options, callable $callback) : void

Groups endpoints with common options.

API::group(['prefix' => 'admin', 'auth' => true], function() {
    API::set('users', 'AdminModule@users');
    API::set('settings', 'AdminModule@settings');
});

API::run(string $page) : bool

Executes an API endpoint. Handles authentication and exceptions automatically.

API::user() : ?object

Returns the currently authenticated user or null.

API::payload() : ?array

Returns the current JWT payload or null.

API::request() : ?array

Returns the current request data.

API::generateToken(int $user_id, array $additional_data = []) : array

Generates a JWT token for a user.

API::refreshToken() : array

Refreshes the current JWT token.

API::successResponse(mixed $data) : void

Sends a success response.

API::errorResponse(string $msg, int $status, ?array $debug_info = null) : void

Sends an error response.

API::listEndpoints() : array

Returns all registered endpoints with their documentation.

API::getDocumentation(string $page) : ?array

Returns documentation for a specific endpoint.

Request Structure

Handlers receive a request array with structured data:

function($request) {
    // HTTP method
    $method = $request['method'];

    // Endpoint page
    $page = $request['page'];

    // URL parameters (id, slug, etc.)
    $id = $request['params']['id'] ?? null;

    // Query parameters (?key=value)
    $search = $request['query']['search'] ?? '';

    // Request body (JSON or form data)
    $data = $request['body'];

    // Headers
    $contentType = $request['headers']['Content-Type'] ?? '';

    // Uploaded files
    $files = $request['files'];

    // Auth info (if authenticated)
    $user = $request['auth']['user'] ?? null;

    // Helper methods
    $value = $request['input']('key', 'default');
    $exists = $request['has']('key');
    $all = $request['all']();
}

Options

Authentication:

  • ['auth' => true] - Require JWT authentication

Permissions:

  • ['permissions' => 'permission.name'] - Require specific permission
  • ['permissions' => 'token'] - Require fixed API token from Config::get('api_token')

HTTP Methods:

  • ['method' => 'GET'] - Only allow GET requests
  • ['method' => 'POST'] - Only allow POST requests
  • ['method' => 'PUT'] - Only allow PUT requests
  • ['method' => 'DELETE'] - Only allow DELETE requests
  • ['method' => 'ANY'] - Allow any method (default)

Authentication Endpoints

auth/login

POST - Obtain JWT token with credentials.

curl -X POST "api.php?page=auth/login" \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "admin"}'
Success response:
{
    "success": true,
    "result": {
        "user": {
            "id": 1,
            "username": "admin",
            "email": "admin@example.com"
        },
        "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
        "expires_at": 1641234567,
        "expires_in": 3600
    }
}

auth/verify

GET - Verify token validity.

curl -X GET "api.php?page=auth/verify" \
-H "Authorization: Bearer YOUR_TOKEN"
Success response:
{
    "success": true,
    "result": {
        "user_id": 1,
        "username": "admin",
        "expires_at": 1641234567
    }
}

auth/refresh

GET - Renew token with existing valid token.

curl -X GET "api.php?page=auth/refresh" \
-H "Authorization: Bearer YOUR_TOKEN"
Success response:
{
    "success": true,
    "result": {
        "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
        "expires_at": 1641237567,
        "expires_in": 3600
    }
}

Error Responses

All errors return HTTP status codes and a standardized error format:

Authentication error (401):

{
    "success": false,
    "message": "No authentication token provided"
}

Permission error (403):

{
    "success": false,
    "message": "Insufficient permissions"
}

Not found (404):

{
    "success": false,
    "message": "Endpoint not found"
}

Method not allowed (405):

{
    "success": false,
    "message": "Method POST not allowed. Expected: GET"
}

Server error (500):

{
    "success": false,
    "message": "Internal server error"
}

Examples

Complete Authenticated API

use App\API;
use App\Exceptions\ApiException;

API::set('posts/create',
    function($request) {
        $title = $request['body']['title'];
        if (empty($title)) {
            throw new ApiException("Title required", 422);
        }
        // Get authenticated user
        $user = API::user();
        $post = new PostsModel();
        $post->fill([
            'title' => $title,
            'content' => $request['body']['content'],
            'user_id' => $user->id
        ]);
        $post->save();

        return [
            'id' => $post->id,
            'title' => $post->title,
            'created_at' => $post->created_at
        ];
    },
    ['auth' => true, 'method' => 'POST'],
    'Create a new blog post',
    ['body' => ['title' => 'string', 'content' => 'string']],
    ['id' => 'int', 'title' => 'string', 'created_at' => 'datetime']
);

PHP Client Example

use App\HttpClient;
use App\Route;

// Login
 $response = HttpClient::post(Route::url() . '/api.php?page=auth/login', [
    'headers' => ['Content-Type' => 'application/json'],
    'body' => json_encode(['username' => 'admin', 'password' => 'admin'])
]);

if ($response['status_code'] == 200 && $response['body']['success']) {
    print "

Success

"; print "
TOKEN: " . $response['body']['data']['token'] . "
"; $user = $response['body']['data']['user']; echo "Username: " . $user['username']; } else { print "

Error

"; echo "
ERROR: " . $response['body']['message'] . "
"; }
Loading...