Development

Pragmatic PHP: Build APIs That Scale Without the Boilerplate

Pragmatic PHP: Build APIs That Scale Without the Boilerplate

Scaling an API is rarely blocked by PHP itself. More often, it is blocked by an application that turned every request into a long chain of hidden work: loading too much data, performing queries inside loops, coupling HTTP concerns to business rules, and treating deployment as a separate problem for later.

Pragmatic PHP is about resisting that drift. It does not mean avoiding structure. It means choosing structure that pays for itself: clear boundaries, predictable data access, observable failures, and a runtime that behaves the same way on a laptop and in production.

Start with a small, explicit request path

An API endpoint should be easy to trace from the route to the response. A useful baseline is to separate transport, application logic, and infrastructure without creating a class for every line of code.

The controller should validate the request and shape the response. A service or action should express the use case. A repository or query object should own persistence details. This keeps framework-specific objects from leaking into the core of the application.

final class CreateOrderAction
{
    public function __construct(
        private OrderRepository $orders,
        private TransactionManager $transactions
    ) {
    }

    public function execute(CreateOrder $command): Order
    {
        return $this->transactions->run(function () use ($command) {
            $order = Order::create(
                customerId: $command->customerId,
                items: $command->items
            );

            $this->orders->save($order);

            return $order;
        });
    }
}

This is not architecture for architecture’s sake. The action makes the unit of work obvious. It gives tests a focused target, and it makes transaction ownership explicit. The controller can translate validation failures into a client response without deciding how orders are persisted.

Make database work visible

Most API performance problems are database problems wearing an application-layer disguise. A fast endpoint is not one that uses clever syntax; it is one that asks the database for the right data in a bounded number of operations.

Watch for the classic N+1 pattern. Fetching a page of orders and then loading a customer or line items for each order may look harmless in local development. Under load, it becomes a query multiplier. Use eager loading, joins, or a purpose-built read query when the response needs related data.

Pagination deserves the same discipline. Offset pagination is familiar, but large offsets can become expensive and inconsistent as rows change. When clients naturally move forward through a stable ordering, cursor pagination is often a better fit. Use a deterministic sort key, include a tie-breaker such as an ID, and encode only the state the next request needs.

  • Index columns used for filtering, joining, and ordering based on real query patterns.
  • Select only fields required by the endpoint; avoid loading an entire record by default.
  • Set sensible page-size limits so one request cannot accidentally become a bulk export.
  • Inspect query plans when a query changes shape or becomes a production hotspot.

Transactions should be short. Do not hold a database transaction open while calling another service, sending email, or generating a report. Persist the state change first, then hand off follow-up work through a reliable mechanism appropriate to the system’s delivery guarantees.

Design APIs for change, not just the first client

HTTP APIs become contracts quickly. Consistency is more valuable than a clever endpoint name. Pick conventions for resource naming, error payloads, pagination, timestamps, and identifiers, then apply them everywhere.

Validation errors should help clients correct a request. Conflict responses should communicate a real state conflict, not conceal an unexpected exception. Server errors should be logged with enough request context to investigate, while the client receives a safe, stable message.

Idempotency matters whenever clients may retry writes. Networks fail, load balancers time out, and users double-submit forms. For an operation such as creating a payment or provisioning an account, an idempotency key lets the server recognize a repeated request and return the result of the original operation instead of performing it twice. Store the key with the relevant request identity and result, and define how long it remains valid.

Versioning should be deliberate, but it should not be the first response to every change. Add optional fields when clients can safely ignore them. Introduce a new endpoint when semantics truly differ. Reserve a new version for changes that existing clients cannot interpret safely.

Use Docker to remove environment surprises

Containers are most useful when they reduce differences between development, testing, and deployment. A PHP application image should contain the required PHP extensions, application code, and a predictable startup command. Configuration that differs by environment should come from environment variables or managed configuration, not from edited images.

FROM php:8.3-fpm-alpine

WORKDIR /app

COPY composer.json composer.lock ./
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
    && php composer-setup.php --install-dir=/usr/local/bin --filename=composer \
    && rm composer-setup.php \
    && composer install --no-dev --prefer-dist --no-interaction

COPY . .

CMD ["php-fpm"]

The exact image strategy will vary by deployment platform, but the principle remains: build once, configure at runtime, and keep the image reproducible. In practice, production images also need a non-root runtime user, appropriate file permissions, and a process model that matches the web server or platform in front of PHP-FPM.

Cache carefully and measure before celebrating

Caching can make a good API cheaper and faster. It can also make a confusing API harder to debug. Start with data that is expensive to calculate, read frequently, and safe to serve slightly stale. Define the cache key, expiration, invalidation path, and failure behavior before adding it.

A cache should not become the only place a request can succeed. If it is unavailable, the application should either fall back safely to the source of truth or fail clearly when freshness is essential. Avoid caching errors unless that behavior is intentional and short-lived.

Measure endpoint latency, error rates, database timing, queue depth where relevant, and resource saturation. Logs should include a request or correlation identifier. Metrics tell you that a problem exists; traces and structured logs help explain which dependency or code path caused it.

Keep the codebase easy to change

Maintainability is a performance feature for teams. Small methods, direct names, and tests around important behavior make it safer to improve an API months after the original implementation. Prefer tests that exercise real boundaries where failures are likely: validation, authorization, persistence, serialization, and integration with infrastructure adapters.

Do not chase abstraction before repetition has revealed a stable concept. A straightforward query object is usually better than a generic repository that cannot express the query you actually need. A focused action is usually better than a sprawling service that owns unrelated workflows.

The boring path is often the scalable one

APIs scale when their behavior is understandable under pressure. Keep request paths explicit, bound database work, make retries safe, package runtime dependencies predictably, and observe the system before tuning it. PHP is entirely capable of serving demanding systems when the surrounding engineering is disciplined.

The goal is not a codebase that looks sophisticated in a diagram. It is a system that can be changed, debugged, and operated with confidence. That is the kind of scalability that lasts.

Blog author portrait

Mihajlo

I’m Mihajlo — a developer driven by curiosity, discipline, and the constant urge to create something meaningful. I share insights, tutorials, and free services to help others simplify their work and grow in the ever-evolving world of software and AI.