Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Api Kit Laravel Package

bulatronic/api-kit

View on GitHub
Deep Wiki
Context7

Usage Examples

Real-world examples based on a working Users + Posts blog API.

Note. This is an intentionally simple blog demo. Architectural patterns such as CQRS, domain events, hexagonal layers, or dedicated write/read models are deliberately omitted to keep the focus on ApiKit itself — not on project structure decisions.

Table of Contents


Users CRUD

A complete Users resource: Entity → Repository → DTOs → Service → Controller.

Entity

<?php

declare(strict_types=1);

namespace App\Entity;

use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: 'users')]
class User
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 180, unique: true)]
    private string $email = '';

    #[ORM\Column(length: 100)]
    private string $name = '';

    public function getId(): ?int { return $this->id; }
    public function getEmail(): string { return $this->email; }
    public function setEmail(string $email): static { $this->email = $email; return $this; }
    public function getName(): string { return $this->name; }
    public function setName(string $name): static { $this->name = $name; return $this; }
}

Repository

<?php

declare(strict_types=1);

namespace App\Repository;

use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/** [@extends](https://github.com/extends) ServiceEntityRepository<User> */
class UserRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, User::class);
    }

    public function existsByEmail(string $email, ?int $excludeId = null): bool
    {
        $qb = $this->createQueryBuilder('u')
            ->select('1')
            ->where('u.email = :email')
            ->setParameter('email', $email);

        if ($excludeId !== null) {
            $qb->andWhere('u.id != :id')->setParameter('id', $excludeId);
        }

        return $qb->getQuery()->getOneOrNullResult() !== null;
    }
}

DTOs

<?php

declare(strict_types=1);

namespace App\Dto\User;

use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;

#[OA\Schema(description: 'Create user payload', required: ['email', 'name'])]
final readonly class CreateUserDto
{
    public function __construct(
        #[Assert\NotBlank]
        #[Assert\Email]
        #[Assert\Length(max: 180)]
        public string $email,

        #[Assert\NotBlank]
        #[Assert\Length(min: 1, max: 100)]
        public string $name,
    ) {
    }
}
<?php

declare(strict_types=1);

namespace App\Dto\User;

use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;

#[OA\Schema(description: 'Update user payload (all fields optional)')]
final readonly class UpdateUserDto
{
    public function __construct(
        #[Assert\Email]
        #[Assert\Length(max: 180)]
        public ?string $email = null,

        #[Assert\Length(min: 1, max: 100)]
        public ?string $name = null,
    ) {
    }
}
<?php

declare(strict_types=1);

namespace App\Dto\User;

use App\Entity\User;
use OpenApi\Attributes as OA;

#[OA\Schema(description: 'User response')]
final readonly class UserResponseDto
{
    public function __construct(
        public int $id,
        public string $email,
        public string $name,
    ) {
    }

    public static function fromEntity(User $user): self
    {
        return new self(
            id: $user->getId(),
            email: $user->getEmail(),
            name: $user->getName(),
        );
    }
}

Service

<?php

declare(strict_types=1);

namespace App\Service;

use ApiKit\Exception\ApiException;
use App\Dto\User\CreateUserDto;
use App\Dto\User\UpdateUserDto;
use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

final readonly class UserService
{
    public function __construct(
        private UserRepository $userRepository,
    ) {
    }

    /** [@return](https://github.com/return) list<User> */
    public function findAll(): array
    {
        return $this->userRepository->findBy([], ['id' => 'ASC']);
    }

    public function findOrFail(int $id): User
    {
        $user = $this->userRepository->find($id);
        if ($user === null) {
            throw new NotFoundHttpException('User not found');
        }
        return $user;
    }

    public function create(CreateUserDto $dto): User
    {
        if ($this->userRepository->existsByEmail($dto->email)) {
            throw new ApiException(409, 'User with this email already exists', [
                'field' => 'email',
                'value' => $dto->email,
            ]);
        }

        $user = new User();
        $user->setEmail($dto->email);
        $user->setName($dto->name);
        $this->userRepository->getEntityManager()->persist($user);
        $this->userRepository->getEntityManager()->flush();

        return $user;
    }

    public function update(int $id, UpdateUserDto $dto): User
    {
        $user = $this->findOrFail($id);

        if ($dto->email !== null) {
            if ($this->userRepository->existsByEmail($dto->email, $id)) {
                throw new ApiException(409, 'User with this email already exists', [
                    'field' => 'email',
                    'value' => $dto->email,
                ]);
            }
            $user->setEmail($dto->email);
        }
        if ($dto->name !== null) {
            $user->setName($dto->name);
        }

        $this->userRepository->getEntityManager()->flush();

        return $user;
    }

    public function delete(int $id): void
    {
        $user = $this->findOrFail($id);
        $this->userRepository->getEntityManager()->remove($user);
        $this->userRepository->getEntityManager()->flush();
    }
}

Controller

<?php

declare(strict_types=1);

namespace App\Controller\Api;

use ApiKit\Controller\AbstractApiController;
use App\Dto\User\CreateUserDto;
use App\Dto\User\UpdateUserDto;
use App\Dto\User\UserResponseDto;
use App\Service\UserService;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/api/users', name: 'api_users_')]
final class UserController extends AbstractApiController
{
    public function __construct(
        private readonly UserService $userService,
    ) {
    }

    #[Route('', name: 'list', methods: ['GET'])]
    public function list(): JsonResponse
    {
        $users = $this->userService->findAll();

        return $this->respondSuccess(array_map(UserResponseDto::fromEntity(...), $users));
    }

    #[Route('/{id}', name: 'get', requirements: ['id' => '\d+'], methods: ['GET'])]
    public function get(int $id): JsonResponse
    {
        return $this->respondSuccess(UserResponseDto::fromEntity($this->userService->findOrFail($id)));
    }

    #[Route('', name: 'create', methods: ['POST'])]
    public function create(#[MapRequestPayload] CreateUserDto $dto): JsonResponse
    {
        return $this->respondCreated(UserResponseDto::fromEntity($this->userService->create($dto)));
    }

    #[Route('/{id}', name: 'update', requirements: ['id' => '\d+'], methods: ['PUT'])]
    public function update(int $id, #[MapRequestPayload] UpdateUserDto $dto): JsonResponse
    {
        return $this->respondSuccess(UserResponseDto::fromEntity($this->userService->update($id, $dto)));
    }

    #[Route('/{id}', name: 'delete', requirements: ['id' => '\d+'], methods: ['DELETE'])]
    public function delete(int $id): JsonResponse
    {
        $this->userService->delete($id);

        return $this->respondNoContent();
    }
}

HTTP Examples

POST /api/users — create a user

POST /api/users
Content-Type: application/json

{
    "email": "author@example.com",
    "name": "Mr. Author"
}
HTTP/1.1 201 Created

{
    "success": true,
    "data": {
        "id": 1,
        "email": "author@example.com",
        "name": "Mr. Author"
    },
    "meta": {
        "timestamp": "2026-02-26T11:21:42+00:00"
    }
}

GET /api/users/1 — found

HTTP/1.1 200 OK

{
    "success": true,
    "data": {
        "id": 1,
        "email": "author@example.com",
        "name": "Mr. Author"
    },
    "meta": {
        "timestamp": "2026-02-26T11:24:51+00:00"
    }
}

GET /api/users/99 — not found (NotFoundHttpExceptionExceptionListener)

HTTP/1.1 404 Not Found

{
    "success": false,
    "error": {
        "code": "NOT_FOUND",
        "message": "User not found"
    }
}

Posts CRUD with EntityExists

Posts are related to Users. The authorId field is validated with EntityExists directly in the DTO — the service receives a guaranteed-valid object.

Entity

<?php

declare(strict_types=1);

namespace App\Entity;

use App\Repository\PostRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: PostRepository::class)]
#[ORM\Table(name: 'posts')]
class Post
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private string $title = '';

    #[ORM\Column(type: Types::TEXT)]
    private string $content = '';

    #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'posts')]
    #[ORM\JoinColumn(nullable: false)]
    private ?User $author = null;

    #[ORM\Column]
    public \DateTimeImmutable $createdAt {
        get { return $this->createdAt; }
    }

    public function __construct()
    {
        $this->createdAt = new \DateTimeImmutable();
    }

    public function getId(): ?int { return $this->id; }
    public function getTitle(): string { return $this->title; }
    public function setTitle(string $title): static { $this->title = $title; return $this; }
    public function getContent(): string { return $this->content; }
    public function setContent(string $content): static { $this->content = $content; return $this; }
    public function getAuthor(): ?User { return $this->author; }
    public function setAuthor(?User $author): static { $this->author = $author; return $this; }
}

Repository

<?php

declare(strict_types=1);

namespace App\Repository;

use App\Entity\Post;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/** [@extends](https://github.com/extends) ServiceEntityRepository<Post> */
class PostRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Post::class);
    }
}

DTOs

#[EntityExists] runs a DB query inside the Symfony validator — the field is checked before the controller action executes:

<?php

declare(strict_types=1);

namespace App\Dto\Post;

use ApiKit\Validator\Constraint\EntityExists;
use App\Entity\User;
use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;

#[OA\Schema(description: 'Create post payload', required: ['title', 'content', 'authorId'])]
final readonly class CreatePostDto
{
    public function __construct(
        #[Assert\NotBlank]
        #[Assert\Length(min: 1, max: 255)]
        public string $title,

        #[Assert\NotBlank]
        public string $content,

        #[Assert\NotNull]
        #[EntityExists(User::class)]
        public int $authorId,
    ) {
    }
}
<?php

declare(strict_types=1);

namespace App\Dto\Post;

use ApiKit\Validator\Constraint\EntityExists;
use App\Entity\User;
use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;

#[OA\Schema(description: 'Update post payload (all fields optional)')]
final readonly class UpdatePostDto
{
    public function __construct(
        #[Assert\Length(min: 1, max: 255)]
        public ?string $title = null,

        public ?string $content = null,

        #[EntityExists(User::class)]
        public ?int $authorId = null,
    ) {
    }
}
<?php

declare(strict_types=1);

namespace App\Dto\Post;

use App\Entity\Post;
use OpenApi\Attributes as OA;

#[OA\Schema(description: 'Post in API response')]
final readonly class PostResponseDto
{
    public function __construct(
        public int $id,
        public string $title,
        public string $content,
        public int $authorId,
        public string $authorName,
        public string $createdAt,
    ) {
    }

    public static function fromEntity(Post $post): self
    {
        $author = $post->getAuthor();

        return new self(
            id: (int) $post->getId(),
            title: $post->getTitle(),
            content: $post->getContent(),
            authorId: (int) $author?->getId(),
            authorName: $author !== null ? $author->getName() : '',
            createdAt: $post->createdAt->format(\DateTimeInterface::ATOM),
        );
    }
}

Service

Because EntityExists already guaranteed authorId exists, the service can load the author without a defensive check:

<?php

declare(strict_types=1);

namespace App\Service;

use App\Dto\Post\CreatePostDto;
use App\Dto\Post\UpdatePostDto;
use App\Entity\Post;
use App\Repository\PostRepository;
use App\Repository\UserRepository;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

final readonly class PostService
{
    public function __construct(
        private PostRepository $postRepository,
        private UserRepository $userRepository,
    ) {
    }

    /** [@return](https://github.com/return) list<Post> */
    public function findAll(): array
    {
        return $this->postRepository->findBy([], ['createdAt' => 'DESC', 'id' => 'DESC']);
    }

    public function findOrFail(int $id): Post
    {
        $post = $this->postRepository->find($id);
        if ($post === null) {
            throw new NotFoundHttpException('Post not found');
        }

        return $post;
    }

    public function create(CreatePostDto $dto): Post
    {
        $author = $this->userRepository->find($dto->authorId);
        if ($author === null) {
            throw new NotFoundHttpException('Author not found');
        }

        $post = new Post();
        $post->setTitle($dto->title);
        $post->setContent($dto->content);
        $post->setAuthor($author);
        $this->postRepository->getEntityManager()->persist($post);
        $this->postRepository->getEntityManager()->flush();

        return $post;
    }

    public function update(int $id, UpdatePostDto $dto): Post
    {
        $post = $this->findOrFail($id);

        if ($dto->title !== null) {
            $post->setTitle($dto->title);
        }
        if ($dto->content !== null) {
            $post->setContent($dto->content);
        }
        if ($dto->authorId !== null) {
            $author = $this->userRepository->find($dto->authorId);
            if ($author === null) {
                throw new NotFoundHttpException('Author not found');
            }
            $post->setAuthor($author);
        }

        $this->postRepository->getEntityManager()->flush();

        return $post;
    }

    public function delete(int $id): void
    {
        $post = $this->findOrFail($id);
        $this->postRepository->getEntityManager()->remove($post);
        $this->postRepository->getEntityManager()->flush();
    }
}

Controller

<?php

declare(strict_types=1);

namespace App\Controller\Api;

use ApiKit\Controller\AbstractApiController;
use App\Dto\Post\CreatePostDto;
use App\Dto\Post\PostResponseDto;
use App\Dto\Post\UpdatePostDto;
use App\Service\PostService;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;

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

    #[Route('', name: 'list', methods: ['GET'])]
    public function list(): JsonResponse
    {
        $posts = $this->postService->findAll();

        return $this->respondSuccess(array_map(PostResponseDto::fromEntity(...), $posts));
    }

    #[Route('/{id}', name: 'get', requirements: ['id' => '\d+'], methods: ['GET'])]
    public function get(int $id): JsonResponse
    {
        return $this->respondSuccess(PostResponseDto::fromEntity($this->postService->findOrFail($id)));
    }

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

    #[Route('/{id}', name: 'update', requirements: ['id' => '\d+'], methods: ['PUT'])]
    public function update(int $id, #[MapRequestPayload] UpdatePostDto $dto): JsonResponse
    {
        return $this->respondSuccess(PostResponseDto::fromEntity($this->postService->update($id, $dto)));
    }

    #[Route('/{id}', name: 'delete', requirements: ['id' => '\d+'], methods: ['DELETE'])]
    public function delete(int $id): JsonResponse
    {
        $this->postService->delete($id);

        return $this->respondNoContent();
    }
}

HTTP Examples

POST /api/posts — create with valid author

POST /api/posts
Content-Type: application/json

{
    "title": "Getting Started with Web Development",
    "content": "Web development is an exciting field...",
    "authorId": 1
}
HTTP/1.1 201 Created

{
    "success": true,
    "data": {
        "id": 1,
        "title": "Getting Started with Web Development",
        "content": "Web development is an exciting field...",
        "authorId": 1,
        "authorName": "Mr. Author",
        "createdAt": "2026-02-26T11:23:14+00:00"
    },
    "meta": {
        "timestamp": "2026-02-26T11:23:14+00:00"
    }
}

POST /api/posts — non-existent authorId (EntityExists fails → 422)

POST /api/posts
Content-Type: application/json

{
    "title": "10 Tips for Better Time Management",
    "content": "Do you often feel like there aren't enough hours...",
    "authorId": 3
}
HTTP/1.1 422 Unprocessable Entity

{
    "success": false,
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Validation error",
        "details": {
            "violations": [
                {
                    "field": "authorId",
                    "message": "Entity \"User\" with id = \"3\" not found."
                }
            ]
        }
    }
}

GET /api/posts — list (sorted by createdAt DESC)

HTTP/1.1 200 OK

{
    "success": true,
    "data": [
        {
            "id": 2,
            "title": "10 Tips for Better Time Management",
            "content": "Do you often feel like there aren't enough hours...",
            "authorId": 1,
            "authorName": "Mr. Author",
            "createdAt": "2026-02-26T11:28:05+00:00"
        },
        {
            "id": 1,
            "title": "Getting Started with Web Development",
            "content": "Web development is an exciting field...",
            "authorId": 1,
            "authorName": "Mr. Author",
            "createdAt": "2026-02-26T11:23:14+00:00"
        }
    ],
    "meta": {
        "timestamp": "2026-02-26T11:28:08+00:00"
    }
}

ApiException for Business Rules

Use ApiException when you need to return a structured error with custom details — for example, a unique-constraint violation with the conflicting field name:

use ApiKit\Exception\ApiException;

public function create(CreateUserDto $dto): User
{
    if ($this->userRepository->existsByEmail($dto->email)) {
        throw new ApiException(409, 'User with this email already exists', [
            'field' => 'email',
            'value' => $dto->email,
        ]);
    }
    // ...
}
HTTP/1.1 409 Conflict

{
    "success": false,
    "error": {
        "code": "CONFLICT",
        "message": "User with this email already exists",
        "details": {
            "field": "email",
            "value": "author@example.com"
        }
    }
}

The controller needs no try/catchExceptionListener handles it automatically:

#[Route('', name: 'create', methods: ['POST'])]
public function create(#[MapRequestPayload] CreateUserDto $dto): JsonResponse
{
    return $this->respondCreated(UserResponseDto::fromEntity($this->userService->create($dto)));
}

Pagination with Meta

#[Route('', methods: ['GET'])]
public function list(#[MapQueryString] PostsQueryDto $query): JsonResponse
{
    $result = $this->postService->paginate($query->page, $query->perPage);

    return $this->respondSuccess(
        data: $result->items,
        meta: [
            'pagination' => [
                'total'       => $result->total,
                'page'        => $result->page,
                'per_page'    => $result->perPage,
                'total_pages' => $result->totalPages,
            ],
        ],
    );
}

Response:

{
    "success": true,
    "data": [...],
    "meta": {
        "timestamp": "2026-02-26T12:00:00+00:00",
        "pagination": {
            "total": 100,
            "page": 1,....
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
codifyo/ts-generator-bundle
andydefer/laravel-cluster
testo/fiber
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky
spatie/mailcoach-vapor