PHPackages                             bulatronic/api-kit - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. bulatronic/api-kit

ActiveSymfony-bundle[HTTP &amp; Networking](/categories/http)

bulatronic/api-kit
==================

Lightweight Symfony bundle for building REST APIs with DTO-first validation, standardized responses, and thin controllers

1.3.1(3mo ago)456↓90%MITPHPPHP &gt;=8.2

Since Feb 25Pushed 3mo agoCompare

[ Source](https://github.com/bulatronic/api-kit)[ Packagist](https://packagist.org/packages/bulatronic/api-kit)[ RSS](/packages/bulatronic-api-kit/feed)WikiDiscussions master Synced 3w ago

READMEChangelogDependencies (19)Versions (11)Used By (0)

ApiKit Symfony Bundle
=====================

[](#apikit-symfony-bundle)

[![PHP Version](https://camo.githubusercontent.com/744f8821cc27dec8b0013ade48179731a44eadf4f943e0b1d9ffcb93f80177de/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d382e322532422d626c75652e737667)](https://php.net)[![Symfony Version](https://camo.githubusercontent.com/7fa456da03fbb18358cf4984b5c378a92433931501c427db621e83f86694c8a2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f73796d666f6e792d372e342d677265656e2e737667)](https://symfony.com)[![License](https://camo.githubusercontent.com/074b89bca64d3edc93a1db6c7e3b1636b874540ba91d66367c0e5e354c56d0ea/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d627269676874677265656e2e737667)](LICENSE)

A minimalist Symfony Bundle for building REST APIs with standardized responses, automatic exception handling, and DTO validation.

Key Features
------------

[](#key-features)

- **Standardized JSON responses** — unified format for all endpoints
- **Automatic exception handling** — exceptions become JSON without `try/catch` in controllers
- **`ApiException`** — throw structured errors with details from anywhere in the codebase
- **DTO validation** — uses native Symfony `#[MapRequestPayload]` / `#[MapQueryString]`
- **`EntityExists` validator** — check entity existence directly in DTO
- **File uploads** — `#[MapUploadedFile]` with automatic validation error handling (images, videos, mixed multipart)
- **`AbstractApiController`** + **`ApiControllerTrait`** — convenient response helpers
- **PHP 8.2 + Symfony 7.4** — modern features, minimal dependencies

Architecture Compatibility
--------------------------

[](#architecture-compatibility)

ApiKit only standardizes the **HTTP layer** — responses and exception handling. It has no opinion on how the rest of your application is organized.

ArchitectureHow ApiKit fits**Layered / Traditional**Controller → Service → Repository. Controllers use `AbstractApiController`, services throw exceptions.**DDD**ApiKit lives in the infrastructure/presentation layer. The domain knows nothing about it — domain services throw standard PHP exceptions, `ExceptionListener` catches them outside.**Hexagonal (Ports &amp; Adapters)**`AbstractApiController` is a driving adapter. The application core (ports + domain) has zero dependency on ApiKit.**Vertical Slice Architecture**`ApiControllerTrait` is the natural fit — each slice is an independent class with no shared inheritance. The trait adds `respond*` methods without forcing a class hierarchy.---

Before &amp; After
------------------

[](#before--after)

**Without ApiKit** — boilerplate in every controller:

```
public function create(Request $request): JsonResponse
{
    try {
        $dto = $this->serializer->deserialize($request->getContent(), CreatePostDto::class, 'json');
        $errors = $this->validator->validate($dto);
        if (count($errors) > 0) {
            return $this->json(['error' => (string) $errors], 422);
        }
        $result = $this->service->create($dto);
        return $this->json(['success' => true, 'data' => $result], 201);
    } catch (ConflictException $e) {
        return $this->json(['error' => $e->getMessage()], 409);
    } catch (\Throwable $e) {
        $this->logger->error($e->getMessage());
        return $this->json(['error' => 'Internal error'], 500);
    }
}
```

**With ApiKit** — one line, same result:

```
public function create(#[MapRequestPayload] CreatePostDto $dto): JsonResponse
{
    return $this->respondCreated($this->service->create($dto));
}
```

Validation, exception handling, and logging are handled automatically and uniformly across all endpoints.

Installation
------------

[](#installation)

```
composer require bulatronic/api-kit
```

The bundle is automatically registered via Symfony Flex.

Quick Start
-----------

[](#quick-start)

### 1. Create a DTO

[](#1-create-a-dto)

```
final readonly class CreatePostDto
{
    public function __construct(
        #[Assert\NotBlank]
        #[Assert\Length(min: 3, max: 255)]
        public string $title,

        #[Assert\NotBlank]
        public string $content,
    ) {}
}
```

### 2. Create a Controller

[](#2-create-a-controller)

Two options — pick one:

**Option A: extend `AbstractApiController`** (simplest, when you don't extend another class):

```
use ApiKit\Controller\AbstractApiController;

#[Route('/api/posts')]
final class PostController extends AbstractApiController
{
    public function __construct(
        private readonly PostService $postService,
    ) {}

    #[Route('', methods: ['GET'])]
    public function list(): JsonResponse
    {
        return $this->respondSuccess($this->postService->findAll());
    }

    #[Route('', methods: ['POST'])]
    public function create(#[MapRequestPayload] CreatePostDto $dto): JsonResponse
    {
        return $this->respondCreated($this->postService->create($dto));
    }

    #[Route('/{id}', methods: ['DELETE'])]
    public function delete(int $id): JsonResponse
    {
        $this->postService->delete($id);
        return $this->respondNoContent();
    }
}
```

**Option B: use `ApiControllerTrait`** (when you already extend another class):

```
use ApiKit\Controller\ApiControllerTrait;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

#[Route('/api/posts')]
final class PostController extends AbstractController
{
    use ApiControllerTrait;

    // ... same methods
}
```

### 3. Throw Errors from Services — No try/catch Needed

[](#3-throw-errors-from-services--no-trycatch-needed)

```
use ApiKit\Exception\ApiException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

final class PostService
{
    public function findOrFail(int $id): Post
    {
        $post = $this->repository->find($id);

        if (null === $post) {
            throw new NotFoundHttpException('Post not found');
        }

        return $post;
    }

    public function create(CreatePostDto $dto): Post
    {
        if ($this->repository->existsByTitle($dto->title)) {
            throw new ApiException(409, 'Post with this title already exists', [
                'field' => 'title',
                'value' => $dto->title,
            ]);
        }

        // ...
    }
}
```

`ExceptionListener` catches everything automatically. Your controller stays clean:

```
public function create(#[MapRequestPayload] CreatePostDto $dto): JsonResponse
{
    return $this->respondCreated($this->postService->create($dto));
}
```

### 4. Standardized Response Format

[](#4-standardized-response-format)

**Success (200):**

```
{
    "success": true,
    "data": [{"id": 1, "title": "Post 1"}],
    "meta": {"timestamp": "2026-02-23T12:00:00+00:00"}
}
```

**Validation error (422) — from `#[MapRequestPayload]`:**

```
{
    "success": false,
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Validation error",
        "details": {
            "violations": [
                {"field": "title", "message": "This value should not be blank."}
            ]
        }
    }
}
```

**`ApiException` error (409):**

```
{
    "success": false,
    "error": {
        "code": "CONFLICT",
        "message": "Post with this title already exists",
        "details": {
            "field": "title",
            "value": "My Post"
        }
    }
}
```

Reference
---------

[](#reference)

### `AbstractApiController` / `ApiControllerTrait` Methods

[](#abstractapicontroller--apicontrollertrait-methods)

```
// Success responses
$this->respondSuccess($data, $status = 200, $meta = []);
$this->respondCreated($data, $meta = []);
$this->respondNoContent();

// Error responses
$this->respondError($message, $status = 400, $code = 'ERROR', $details = []);
$this->respondNotFound($message = 'Resource not found');
$this->respondForbidden($message = 'Access forbidden');
$this->respondUnauthorized($message = 'Unauthorized');
```

### `ResponseFactory` (inject as dependency)

[](#responsefactory-inject-as-dependency)

```
public function __construct(
    private readonly ResponseFactory $responseFactory,
) {}

$this->responseFactory->success($data, $statusCode = 200, $meta = []);
$this->responseFactory->created($data, $meta = []);
$this->responseFactory->noContent();
$this->responseFactory->error($message, $code = 'ERROR', $statusCode = 400, $details = []);
```

### `ApiException`

[](#apiexception)

```
use ApiKit\Exception\ApiException;

// Without details
throw new ApiException(409, 'Email already taken');

// With structured details
throw new ApiException(423, 'Account locked', [
    'locked_until'  => $until->format(\DateTimeInterface::ATOM),
    'reason'        => 'too_many_attempts',
]);
```

### `EntityExists` Validator

[](#entityexists-validator)

**Requires Doctrine ORM:**

```
composer require doctrine/orm doctrine/doctrine-bundle
```

```
use ApiKit\Validator\Constraint\EntityExists;

final readonly class CreateCommentDto
{
    public function __construct(
        #[Assert\NotBlank]
        public string $content,

        #[Assert\Uuid]
        #[EntityExists(User::class)]
        public string $authorId,

        // Search by field other than id
        #[EntityExists(entityClass: Category::class, field: 'slug')]
        public ?string $categorySlug = null,
    ) {}
}
```

### Exception Handling

[](#exception-handling)

`ExceptionListener` handles automatically (no `try/catch` in controllers needed):

ExceptionHTTP StatusNotes`ValidationFailedException`422From `#[MapRequestPayload]` or manual`HttpException(*, prev: ValidationFailed)`Same as exceptionViolations extracted`ApiException`Configured code`getDetails()` included in responseAny `HttpExceptionInterface`Status from exceptionStandard Symfony exceptionsAny other `\Throwable`500Logged; trace shown in debug modeConfiguration
-------------

[](#configuration)

Create `config/packages/api_kit.yaml` (optional — sensible defaults work out of the box):

```
api_kit:
    response:
        include_timestamp: true           # Include timestamp in responses
        pretty_print: '%kernel.debug%'    # Pretty-print JSON in debug mode

    exception_handling:
        log_errors: true                  # Log server errors (5xx only)
        show_trace: '%kernel.debug%'      # Include stack trace in 500 responses
```

Testing
-------

[](#testing)

```
# Run tests
composer test

# PHPStan static analysis
composer phpstan

# Check code style
composer cs-check

# Fix code style
composer cs-fix
```

Requirements
------------

[](#requirements)

- PHP 8.2+
- Symfony 7.4+

**Optional:**

- Doctrine ORM — required for `EntityExists` validator

Documentation
-------------

[](#documentation)

- [Usage Examples](docs/examples.md)
- [Architecture](docs/ARCHITECTURE.md)
- [Development Guide](docs/DEVELOPMENT.md)
- [Contributing](CONTRIBUTING.md)

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

Author
------

[](#author)

**Bulat Timerbaev** —

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance78

Regular maintenance activity

Popularity12

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity53

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

10

Last Release

115d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/5a95ee910d04cb9ea813dd273f28d77d5dead2e906e9ac708f81c1499d2fa746?d=identicon)[bulatronic](/maintainers/bulatronic)

---

Top Contributors

[![bulatronic](https://avatars.githubusercontent.com/u/42603474?v=4)](https://github.com/bulatronic "bulatronic (12 commits)")

---

Tags

apisymfonybundlevalidationrestdto

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/bulatronic-api-kit/health.svg)

```
[![Health](https://phpackages.com/badges/bulatronic-api-kit/health.svg)](https://phpackages.com/packages/bulatronic-api-kit)
```

###  Alternatives

[api-platform/core

Build a fully-featured hypermedia or GraphQL API in minutes!

2.6k50.1M314](/packages/api-platform-core)[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.5M373](/packages/easycorp-easyadmin-bundle)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M196](/packages/sulu-sulu)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.4M517](/packages/shopware-core)[web-auth/webauthn-framework

FIDO2/Webauthn library for PHP and Symfony Bundle.

51090.8k2](/packages/web-auth-webauthn-framework)[drupal/core

Drupal is an open source content management platform powering millions of websites and applications.

19564.8M1.6k](/packages/drupal-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
