Docs

REST & GraphQL APIs

MonkeysLegion lets you expose both REST and GraphQL from the same domain model with almost no boilerplate.

Use REST for simple, cache-friendly endpoints; switch to GraphQL when clients need flexible, data-dense queries.

1 · Choosing the right style

Trait

REST

GraphQL

Verbosity

Multiple routes—one per resource action

Single /graphql endpoint

Caching

Easy (GET) at CDN / browser

Needs persisted queries or APQ

Over-fetch / under-fetch

Possible; define more routes

Client controls shape

Batching

Parallel HTTP calls

One round-trip

Error surface

Status codes (4xx/5xx)

200 + errors[] payload

Most teams start with REST (quick to reason about), then add GraphQL for front-end heavy screens or mobile apps that benefit from tailor-made payloads.

Part A — Building a REST API

A.1  Controller pattern

use MonkeysLegion\Router\Attribute\Route;
use Psr\Http\Message\ResponseInterface;
use Laminas\Diactoros\Response\JsonResponse;

final class PostController
{
    #[Route('GET', '/posts')]
    public function index(PostRepository $posts): ResponseInterface
    {
        return new JsonResponse($posts->latest());
    }

    #[Route('POST', '/posts')]
    public function store(CreatePost $dto, PostService $svc): ResponseInterface
    {
        $id = $svc->create($dto);
        return new JsonResponse(['id'=>$id], 201);
    }

    #[Route('GET', '/posts/{id:\d+}')]
    public function show(int $id, PostRepository $posts): ResponseInterface
    {
        $post = $posts->find($id) ?? throw new HttpNotFoundException();
        return new JsonResponse($post);
    }
}
  • DTO binding + validation comes from ValidationMiddleware.

  • HttpNotFoundException (or any throwable) is turned into a JSON problem-detail response by the ExceptionMiddleware.

A.2  Pagination & filters

$rows = $qb->select('*')
           ->from('posts')
           ->where('status','=','published')
           ->limit($request->query('per_page', 20))
           ->offset(($request->query('page', 1) - 1) * $perPage)
           ->fetchAll(Post::class);

return new JsonResponse([
    'data'  => $rows,
    'meta'  => ['page'=>$page, 'per_page'=>$perPage, 'total'=>$total],
    'links' => [/* prev / next URLs */],
]);

Add ?sort=-published_at&author=42 filters with where() helpers or a Query object pattern.

A.3  Versioning

  • URI style: /v2/posts — simplest, CDN-friendly.

  • Header style: Accept: application/vnd.ml.v2+json — keeps URLs stable.

  • Route grouping:

$router->group('/v2', fn() => require base_path('routes/v2.php'));

A.4  OpenAPI generation

$kernel->add(new OpenApiMiddleware(
    generator: new OpenApiGenerator($routeCollection),
    jsonPath: '/openapi.json',
));

Swagger UI or Stoplight can consume /openapi.json instantly.

Part B — Adding GraphQL

B.1  Install adapter

composer require webonyx/graphql-php monkeyscloud/monkeyslegion-graphql

(The -graphql bridge registers a controller, schema builder, and a WebSocket subscription handler.)

B.2  Define types & resolvers

use MonkeysLegion\GraphQL\Attribute\Type;
use GraphQL\Type\Definition\Type as Gql;

#[Type]
final class PostType
{
    public function fields(): array
    {
        return [
            'id'          => Gql::id(),
            'title'       => Gql::string(),
            'body'        => Gql::string(),
            'publishedAt' => Gql::string(),
            'author'      => AuthorType::class,
        ];
    }
}

monkeyslegion-graphql scans app/GraphQL/, registers types, queries, and mutations automatically.

B.3  Root query & mutation

#[Type]
final class Query
{
    public function fields(): array
    {
        return [
            'posts' => [
                'type'    => Type::listOf(Type::nonNull(PostType::class)),
                'resolve' => fn($root, $args, $ctx) =>
                    $ctx->posts->latest(),
            ],
        ];
    }
}

#[Type]
final class Mutation
{
    public function fields(): array
    {
        return [
            'createPost' => [
                'type' => PostType::class,
                'args' => [
                    'title' => Gql::nonNull(Gql::string()),
                    'body'  => Gql::nonNull(Gql::string()),
                ],
                'resolve' => fn($root, $args, $ctx) =>
                    $ctx->postSvc->create($args['title'], $args['body']),
            ],
        ];
    }
}

B.4  Register endpoint

routes/api.php

#[Route('POST','/graphql')]
class GraphQLController
{
    public function __construct(
        private \MonkeysLegion\GraphQL\Executor $executor
    ){}

    public function __invoke(ServerRequestInterface $r): JsonResponse
    {
        return new JsonResponse($this->executor->execute($r), 200);
    }
}

Launch GraphiQL at http://localhost:8000/graphiql (served automatically in dev).

B.5  Securing with JWT & policies

  • Re-use JwtAuthMiddleware to decode token and set $context['user'].

  • Wrap resolvers with a simple helper:

$auth->can('edit', $post) || throw new \GraphQL\Error\Error('Forbidden');

Part C — Best practices

Concern

REST

GraphQL

Auth

Bearer token in Authorization header

Same header; WebSocket uses connectionParams

Rate-limit

RateLimitMiddleware — per IP/token

Ditto (per‐operation granularity via field name)

Monitoring

Telemetry → http_requests_total

🤝  add graphql_operation_seconds label per query

Errors

JSON Problem Details (type, title, …)

Standard errors array, extensions.code = slug

Next steps

  • Dive into Validation to ensure every mutation input is safe.

  • Explore Events & Telemetry to measure query performance.

  • Add Subscription support (GraphQL over WebSockets) for real-time chat.

Happy API building! 🚀