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! 🚀