A powerful, type-safe REST API framework for PHP 8.1+ with full Nette Framework integration. Define structured API endpoints as PHP classes with automatic parameter validation, response serialization, and built-in permission management.
- Type-safe endpoints - Define full type-hint input parameters with automatic validation
- Schema-based responses - Return typed DTOs that are automatically serialized to JSON
- Automatic endpoint discovery - Endpoints are auto-registered via Nette DI container
- Built-in permission system - Role-based access control with
#[PublicEndpoint]and#[Role]attributes - HTTP method routing - Method names determine HTTP verb (GET, POST, PUT, DELETE)
- Tracy debugger integration - Visual debug panel for API request/response inspection
- CORS handling - Automatic Cross-Origin Resource Sharing support
- Dependency injection - Full DI support via constructor or
#[Inject]attribute - Flash messages - Built-in flash message system for API responses
The package follows a layered architecture with clear separation of concerns:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HTTP Request β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ApiManager β
β βββββββββββββββββ ββββββββββββββββββ βββββββββββββββββββ β
β β URL Router β β CORS Handler β β Tracy Panel β β
β β /api/v1/{ep} β β (Preflight) β β (Debug) β β
β βββββββββ¬ββββββββ ββββββββββββββββββ βββββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Middleware Pipeline β β
β β βββββββββββββββββββββ ββββββββββββββββββββββββββββ β β
β β βPermissionExtensionβ β Custom MatchExtensions β β β
β β β (Auth & Roles) β β (before/after process) β β β
β β βββββββββββββββββββββ ββββββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Endpoint β
β βββββββββββββββββ ββββββββββββββββββ βββββββββββββββββββ β
β β BaseEndpoint β β Method Invoker β β Convention β β
β β (Your Logic) β β (Param Binding)β β (Formatting) β β
β βββββββββ¬ββββββββ ββββββββββββββββββ βββββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Response System β β
β β ββββββββββββββ ββββββββββββββ ββββββββββββββββββββ β β
β β βDTO Objects β β StatusResp β β JsonResponse β β β
β β β(Your Types)β β (Ok/Error) β β (Serialized) β β β
β β ββββββββββββββ ββββββββββββββ ββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HTTP Response β
β (JSON with proper HTTP code) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Component | Description |
|---|---|
| ApiManager | Central orchestrator that handles routing, CORS, middleware execution, and response processing |
| ApiExtension | Nette DI extension for automatic service registration and configuration |
| BaseEndpoint | Abstract base class providing helper methods for sending responses |
| Endpoint | Interface that marks a class as an API endpoint |
| MetaDataManager | Discovers and registers endpoint classes using RobotLoader |
| Convention | Configuration entity for response formatting, date formats, and security settings |
| PermissionExtension | Middleware for authentication and role-based access control |
| MatchExtension | Interface for creating custom middleware (before/after processing hooks) |
| Tracy Panel | Debug panel showing request details, parameters, and response data |
The package includes a comprehensive Tracy debug panel for inspecting API requests during development:
The panel displays:
- HTTP method and request URL
- Raw HTTP input parameters
- Resolved endpoint arguments after type casting
- Response type, HTTP code, and content type
- Complete serialized response data
- Request processing time in milliseconds
It's best to use Composer for installation, and you can also find the package on Packagist and GitHub.
To install, simply use the command:
$ composer require baraja-core/structured-api- PHP 8.1 or higher
- Extensions:
json,mbstring,iconv - Nette Framework 3.x
The package integrates automatically with Nette Framework. A model configuration can be found in the common.neon file inside the root of the package.
If you use PackageRegistrator, the extension is registered automatically. Otherwise, register the extension manually in your config.neon:
extensions:
structuredApi: Baraja\StructuredApi\ApiExtensionstructuredApi:
skipError: false # If true, silently logs API exceptions instead of throwingCreate a class that extends BaseEndpoint with the suffix Endpoint:
<?php
declare(strict_types=1);
namespace App\Api;
use Baraja\StructuredApi\BaseEndpoint;
final class ArticleEndpoint extends BaseEndpoint
{
public function __construct(
private ArticleRepository $articleRepository,
) {
}
public function actionDefault(): array
{
return [
'articles' => $this->articleRepository->findAll(),
];
}
public function actionDetail(int $id): ArticleResponse
{
$article = $this->articleRepository->find($id);
return new ArticleResponse(
id: $article->getId(),
title: $article->getTitle(),
content: $article->getContent(),
);
}
}This endpoint will be available at:
GET /api/v1/article- callsactionDefault()GET /api/v1/article/detail?id=123- callsactionDetail(123)
For type-safe responses, create DTO classes:
<?php
declare(strict_types=1);
namespace App\Api\DTO;
final class ArticleResponse
{
public function __construct(
public int $id,
public string $title,
public string $content,
public ?\DateTimeInterface $publishedAt = null,
) {
}
}The DTO is automatically serialized to JSON with proper type handling:
DateTimeInterfaceobjects are formatted according toConvention::$dateTimeFormat- Nested objects and arrays are recursively serialized
- Objects with
__toString()method can be converted to strings
The library supports all HTTP methods for REST APIs. The HTTP method is determined by the method name prefix:
final class UserEndpoint extends BaseEndpoint
{
// GET /api/v1/user
public function actionDefault(): array { }
// GET /api/v1/user (alternative)
public function getDefault(): array { }
// GET /api/v1/user/detail?id=1
public function actionDetail(int $id): UserResponse { }
// POST /api/v1/user/create
public function postCreate(string $name, string $email): UserResponse { }
// POST /api/v1/user/create (alias)
public function createCreate(string $name, string $email): UserResponse { }
// PUT /api/v1/user/update
public function putUpdate(int $id, string $name): UserResponse { }
// PUT /api/v1/user/update (alias)
public function updateUpdate(int $id, string $name): UserResponse { }
// DELETE /api/v1/user/delete
public function deleteDelete(int $id): void { }
}| Prefix | HTTP Method |
|---|---|
action |
GET |
get |
GET |
post |
POST |
create |
POST |
put |
PUT |
update |
PUT |
delete |
DELETE |
Method parameters are automatically validated and type-cast:
final class ArticleEndpoint extends BaseEndpoint
{
/**
* @param string|null $locale in format "cs" or "en"
* @param int $page real page number, 1 = first page
* @param int $limit in interval <1, 500>
*/
public function actionDefault(
?string $locale = null,
int $page = 1,
int $limit = 32,
?string $status = null,
?string $query = null,
?\DateTimeInterface $filterFrom = null,
?\DateTimeInterface $filterTo = null,
?string $sort = null,
?string $orderBy = null,
): ArticleListResponse {
// All parameters are validated and properly typed
}
}Validation rules:
- Parameters with default values are optional
- Parameters without defaults are required - request fails if missing
- Type mismatches trigger automatic conversion or error response
- The special parameter
array $datareceives all raw input data
For complex data structures, use the reserved $data parameter:
public function postProcessOrder(array $data): OrderResponse
{
// $data contains all raw POST data from the request
}BaseEndpoint provides several helper methods for sending responses:
Send raw array data as JSON:
public function actionDetail(int $id): void
{
$this->sendJson([
'id' => $id,
'title' => 'My Article',
'content' => '...',
]);
}Send success response with optional data and message:
public function postCreate(string $title): void
{
$article = $this->repository->create($title);
$this->sendOk(
data: ['id' => $article->getId()],
message: 'Article created successfully',
);
}Response format:
{
"state": "ok",
"message": "Article created successfully",
"code": 200,
"data": {
"id": 123
}
}Send error response:
public function actionDetail(int $id): void
{
$article = $this->repository->find($id);
if ($article === null) {
$this->sendError(
message: 'Article not found',
code: 404,
hint: 'Check if the article ID is correct',
);
}
// ...
}Response format:
{
"state": "error",
"message": "Article not found",
"code": 404,
"hint": "Check if the article ID is correct"
}Send paginated list of items:
public function actionDefault(int $page = 1): void
{
$items = $this->repository->findPage($page);
$paginator = $this->repository->getPaginator();
$this->sendItems($items, $paginator, [
'totalCount' => $paginator->getTotalCount(),
]);
}The preferred approach is returning typed objects directly:
public function actionDetail(int $id): ArticleResponse
{
$article = $this->repository->find($id);
return new ArticleResponse(
id: $article->getId(),
title: $article->getTitle(),
content: $article->getContent(),
);
}This enables:
- Static analysis and IDE support
- Automatic documentation generation
- Type safety at compile time
Add flash messages to any response:
public function postUpdate(int $id, string $title): ArticleResponse
{
$article = $this->repository->update($id, $title);
$this->flashMessage('Article updated successfully', self::FlashMessageSuccess);
$this->flashMessage('Remember to publish your changes', self::FlashMessageInfo);
return new ArticleResponse($article);
}Available flash message types:
FlashMessageSuccess- successFlashMessageInfo- infoFlashMessageWarning- warningFlashMessageError- error
Flash messages are included in the response under flashMessages key:
{
"id": 123,
"title": "Updated Article",
"flashMessages": [
{"message": "Article updated successfully", "type": "success"},
{"message": "Remember to publish your changes", "type": "info"}
]
}All endpoints are private by default. Users must be logged in to access any endpoint unless explicitly marked as public.
Mark an entire endpoint as publicly accessible:
use Baraja\StructuredApi\Attributes\PublicEndpoint;
#[PublicEndpoint]
final class ProductEndpoint extends BaseEndpoint
{
public function actionDefault(): array
{
// Accessible without authentication
}
}Restrict access to specific user roles:
use Baraja\StructuredApi\Attributes\Role;
#[Role(roles: ['admin', 'moderator'])]
final class ArticleEndpoint extends BaseEndpoint
{
// Only admin or moderator can access any method
}Restrict specific methods:
#[PublicEndpoint]
final class ArticleEndpoint extends BaseEndpoint
{
public function actionDefault(): array
{
// Public access
}
#[Role(roles: 'admin')]
public function actionDelete(int $id): void
{
// Only admin can delete
}
#[Role(roles: ['admin', 'editor'])]
public function postCreate(string $title): ArticleResponse
{
// Admin or editor can create
}
}- Check if endpoint has
#[PublicEndpoint]attribute - If not public, require user to be logged in (returns 401 if not)
- Check
#[Role]attributes on class and method - If roles defined, verify user has at least one matching role (returns 403 if not)
- If user is logged in and no roles required, allow access
For special cases, you can disable the default permission checking:
$convention = $container->getByType(Convention::class);
$convention->setIgnoreDefaultPermission(true);The Convention entity controls response formatting and behavior:
$convention = $container->getByType(Convention::class);
// Date/time format for serialization (default: 'Y-m-d H:i:s')
$convention->setDateTimeFormat('c'); // ISO 8601
// Default HTTP codes
$convention->setDefaultErrorCode(500);
$convention->setDefaultOkCode(200);
// Remove null values from response to reduce payload size
$convention->setRewriteNullToUndefined(true);
// Keys to hide from response (sensitive data)
$convention->setKeysToHide([
'password', 'passwd', 'pass', 'pwd',
'creditcard', 'credit card', 'cc', 'pin',
'secret', 'token',
]);
// Use __toString() method when serializing objects
$convention->setRewriteTooStringMethod(true);Create custom middleware by implementing MatchExtension:
use Baraja\StructuredApi\Middleware\MatchExtension;
use Baraja\StructuredApi\Endpoint;
use Baraja\StructuredApi\Response;
final class RateLimitExtension implements MatchExtension
{
public function beforeProcess(
Endpoint $endpoint,
array $params,
string $action,
string $method,
): ?Response {
if ($this->isRateLimited()) {
return new JsonResponse($this->convention, [
'state' => 'error',
'message' => 'Rate limit exceeded',
], 429);
}
return null; // Continue processing
}
public function afterProcess(
Endpoint $endpoint,
array $params,
?Response $response,
): ?Response {
// Modify or replace response after processing
return null; // Use original response
}
}Register the extension:
$apiManager = $container->getByType(ApiManager::class);
$apiManager->addMatchExtension(new RateLimitExtension());Call endpoints programmatically from PHP code:
$apiManager = $container->getByType(ApiManager::class);
// Get response as array
$result = $apiManager->get('article/detail', ['id' => 123]);
// With explicit HTTP method
$result = $apiManager->get('article/create', ['title' => 'New'], 'POST');
// The path is automatically prefixed with 'api/v1/' if not present
$result = $apiManager->get('api/v1/article/detail', ['id' => 123]);API URLs follow this pattern:
/api/v{version}/{endpoint}/{action}?{parameters}
| URL | Endpoint Class | Method Called |
|---|---|---|
/api/v1/article |
ArticleEndpoint |
actionDefault() |
/api/v1/article/detail?id=5 |
ArticleEndpoint |
actionDetail(5) |
/api/v1/user-profile |
UserProfileEndpoint |
actionDefault() |
/api/v1/user-profile/settings |
UserProfileEndpoint |
actionSettings() |
Endpoint class names are converted to URL paths using kebab-case:
ArticleEndpointβ/api/v1/articleUserProfileEndpointβ/api/v1/user-profileMyAwesomeApiEndpointβ/api/v1/my-awesome-api
Action names also use kebab-case in URLs:
actionGetUserProfile()β/api/v1/endpoint/get-user-profile
The package integrates with baraja-core/cas for user management:
final class ProfileEndpoint extends BaseEndpoint
{
public function actionDefault(): UserResponse
{
// Check if user is logged in
if (!$this->isUserLoggedIn()) {
$this->sendError('Not authenticated', 401);
}
// Get current user
$user = $this->getUser();
// Get user identity entity
$identity = $this->getUserEntity();
return new UserResponse($identity);
}
}Generate application links from endpoints:
final class ArticleEndpoint extends BaseEndpoint
{
public function actionDetail(int $id): ArticleResponse
{
$article = $this->repository->find($id);
return new ArticleResponse(
id: $article->getId(),
title: $article->getTitle(),
editUrl: $this->link('Admin:Article:edit', ['id' => $id]),
viewUrl: $this->linkSafe('Front:Article:detail', ['slug' => $article->getSlug()]),
);
}
}link()- Generates link, throws exception if route doesn't existlinkSafe()- Returnsnullif route doesn't exist
For secure communication with external services or partners, use the official token authorizer extension:
structured-api-token-authorizer
Generate API documentation automatically from your endpoint code:
This extension scans your endpoints and generates documentation based on:
- Method signatures and parameters
- PHPDoc annotations
- Return type DTOs
- Permission attributes
The package includes two test endpoints for verification:
GET /api/v1/ping
Response:
{
"result": "PONG",
"ip": "127.0.0.1",
"datetime": "2024-01-15 10:30:00"
}GET /api/v1/test?hello=World
Response:
{
"name": "Test API endpoint",
"hello": "World",
"endpoint": "Test"
}The API automatically handles errors and returns appropriate HTTP codes:
| HTTP Code | Situation |
|---|---|
| 200 | Successful request |
| 400 | Bad request (validation error) |
| 401 | Unauthorized (not logged in) |
| 403 | Forbidden (missing role) |
| 404 | Endpoint or action not found |
| 500 | Internal server error |
Error response format:
{
"state": "error",
"message": "Human readable error message",
"code": 500
}In debug mode (Tracy enabled), the message field contains the actual exception message. In production, a generic error message is shown for security.
Cross-Origin Resource Sharing is automatically handled:
Access-Control-Allow-Origin- Set to request originAccess-Control-Allow-Credentials- EnabledAccess-Control-Max-Age- 86400 seconds (1 day)- OPTIONS preflight requests are handled automatically
Jan Barasek - https://baraja.cz
baraja-core/structured-api is licensed under the MIT license. See the LICENSE file for more details.
