PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

REST API – resources vs actions, HTTP codes, response structure, versioning

by Henryk Tews / Tuesday, 09 February 2021 / Published in PHP

REST API design is one of those topics where everyone has an opinion but few have a consistent set of rules. Resources vs actions, the right HTTP status codes, response structure, versioning – decisions made here stay with the API for years. I show the principles I follow when designing REST APIs in PHP projects and Magento 2 modules.

Resources vs actions – the fundamental distinction

# BAD - RPC style (actions as verbs in URL)
POST /api/cancelOrder/42
POST /api/getProductsByCategory?id=5
GET  /api/activateUser?userId=12

# GOOD - resource-oriented REST
DELETE /api/orders/42          (or PATCH with status change)
GET    /api/products?category=5
PATCH  /api/users/12           {"status": "active"}

HTTP status codes – use them correctly

Code Meaning When to use
200 OK Success GET, PUT, PATCH with response body
201 Created Resource created POST that creates a new resource
204 No Content Success, no body DELETE, PATCH without response body
400 Bad Request Invalid input Validation error, malformed JSON
401 Unauthorized Not authenticated Missing or invalid token
403 Forbidden Not authorised Authenticated but insufficient permissions
404 Not Found Resource not found GET/DELETE on non-existent ID
409 Conflict State conflict Duplicate, optimistic locking violation
422 Unprocessable Semantic validation error Invalid field values (vs 400 for syntax)
500 Internal Error Server error Unhandled exception – never leak details

Consistent response structure

<?php

declare(strict_types=1);

class ApiResponse
{
    // Success response
    public static function success(mixed $data, int $status = 200): array
    {
        return ['data' => $data, 'status' => 'success'];
    }

    // Collection with pagination
    public static function collection(array $items, int $total, int $page, int $perPage): array
    {
        return [
            'data' => $items,
            'meta' => [
                'total'        => $total,
                'per_page'     => $perPage,
                'current_page' => $page,
                'last_page'    => (int) ceil($total / $perPage),
            ],
        ];
    }

    // Error response
    public static function error(string $message, array $errors = [], int $status = 400): array
    {
        $response = ['status' => 'error', 'message' => $message];
        if (!empty($errors)) {
            $response['errors'] = $errors;
        }
        return $response;
    }
}

// Example controller responses
class ProductController
{
    public function show(int $id): JsonResponse
    {
        try {
            $product = $this->productRepository->getById($id);
            return new JsonResponse(ApiResponse::success($product->toArray()), 200);
        } catch (NoSuchEntityException $e) {
            return new JsonResponse(ApiResponse::error('Product not found'), 404);
        }
    }

    public function store(Request $request): JsonResponse
    {
        $errors = $this->validator->validate($request->all());
        if (!empty($errors)) {
            return new JsonResponse(ApiResponse::error('Validation failed', $errors), 422);
        }

        $product = $this->productService->create($request->all());
        return new JsonResponse(ApiResponse::success($product->toArray()), 201);
    }
}

Versioning strategies

# Strategy 1: URL versioning (most common, clearest)
GET /api/v1/products
GET /api/v2/products

# Strategy 2: Header versioning (cleaner URLs, harder to test in browser)
GET /api/products
Accept: application/vnd.api+json; version=2

# Strategy 3: Query parameter (simplest, least REST-like)
GET /api/products?v=2
<?php

// Router configuration for versioned API
$router->prefix('api')->group(function () {
    // v1 - original contracts, never change
    $router->prefix('v1')->group(function () {
        $router->get('products', [V1\ProductController::class, 'index']);
        $router->get('products/{id}', [V1\ProductController::class, 'show']);
    });

    // v2 - new response structure
    $router->prefix('v2')->group(function () {
        $router->get('products', [V2\ProductController::class, 'index']);
        $router->get('products/{id}', [V2\ProductController::class, 'show']);
    });
});

Magento 2 REST API – built-in versioning

<!-- etc/webapi.xml - Magento follows v1/v2 URL convention -->
<route url="/V1/vendor-module/subscriptions" method="GET">
    <service class="Vendor\Module\Api\SubscriptionRepositoryInterface" method="getList"/>
    <resources>
        <resource ref="Magento_Customer::manage"/>
    </resources>
</route>

<route url="/V1/vendor-module/subscriptions/:id" method="GET">
    <service class="Vendor\Module\Api\SubscriptionRepositoryInterface" method="getById"/>
    <resources>
        <resource ref="self"/>
    </resources>
</route>

<route url="/V1/vendor-module/subscriptions" method="POST">
    <service class="Vendor\Module\Api\SubscriptionRepositoryInterface" method="save"/>
    <resources>
        <resource ref="anonymous"/>
    </resources>
</route>

Summary

Consistent REST API design is an investment in the future – every developer who consumes your API will be grateful for predictable URL structure, correct status codes, and uniform error responses. In Magento 2 the webapi.xml system covers versioning and access control automatically, but the response structure quality still depends on how well you design your Service Contracts.

About Henryk Tews

What you can read next

PHP 7.2 – object type hint, sodium instead of mcrypt, deprecations

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}