REST API Review Checklist for Symfony Teams
AI generated
{ }
GET
Code Review · Checklist · Symfony · REST API · Teams
REST API Review Checklist
for Symfony Teams

API reviews without a systematic checklist consistently miss the same categories: security aspects, error formats, performance and test coverage tend to get skipped under time pressure. A structured checklist makes reviews faster, more complete and repeatable, and ensures that no PR gets merged with a critical gap.

15 min read Design · Security · Performance · Errors · Documentation · Tests · Deployment Symfony 7 · PHP 8.4 · OpenAPI 3.1 · PHPUnit · Spectral

1. Design checklist: URLs, verbs and status codes

Design mistakes are the most expensive ones, because they lead to breaking changes if fixed after launch. The design review checks fundamental REST conformance before implementation details are even discussed. A reviewer looking for design problems has a different focus than one checking code quality, which is why both aspects should be kept separate in the checklist.

# API Design Review Checklist - run through every new endpoint

## URL Design
[ ] Resource as a plural noun: /products, /orders, /users
[ ] No verb in the URL: /getProduct -> /products/{id}
[ ] Maximum 2 levels of hierarchy: /orders/{id}/items (OK)
[ ] Sub-resources only when the relationship is truly hierarchical
[ ] Consistent naming (kebab-case or camelCase, never mixed)
[ ] IDs in the path, no /products?id=42 for a single resource

## HTTP Verbs
[ ] GET: no side effects, safe and idempotent
[ ] POST: creates a new resource, returns 201 with Location
[ ] PUT: full replacement (all fields required in the request)
[ ] PATCH: partial update (only fields that were sent)
[ ] DELETE: idempotent (a second delete returns 404, not 500)
[ ] No DELETE parameters in the body (query string or URL instead)

## Status Codes
[ ] 200 only when a resource is actually returned
[ ] 201 on POST (create) with a Location header
[ ] 204 on DELETE and PATCH with no response body
[ ] 400 for syntactically malformed requests (invalid JSON)
[ ] 401 when no token is present (not 403)
[ ] 403 when the token is valid but permission is missing
[ ] 404 when the resource does not exist
[ ] 409 for conflicts (duplicate email, optimistic lock)
[ ] 422 for validation errors (missing required fields, invalid values)
[ ] 429 when the rate limit is exceeded
[ ] No 200 for errors, no 500 for client errors

2. Security checklist: auth, input and output

Security reviews require a different mindset than design or code reviews: you must actively hunt for gaps rather than just check whether the code works. The most common API security problems are missing authorization checks at the resource level (BOLA/IDOR), missing input validation and accidentally exposing internal data in the response. A reviewer who systematically works through the security checklist finds these classes of problems consistently.

The distinction between authentication (who are you?) and authorization (what are you allowed to do?) is particularly important. It is a common mistake to check only whether the user is logged in, not whether they are permitted to read or write the specific resource. A user with a valid JWT should only be able to read /orders/42 if order 42 belongs to their account, and a Symfony voter or an explicit ownership check must guarantee that.

denyAccessUnlessGranted('ORDER_VIEW', $order);
        // ...
    }
}

// [x] Input validation: Symfony Validator on DTOs
// src/Dto/CreateProductRequest.php
final readonly class CreateProductRequest
{
    public function __construct(
        #[NotBlank]
        #[Length(max: 255)]
        public string $name,

        #[NotBlank]
        #[Positive]
        public float $price,

        #[NotBlank]
        #[Regex(pattern: '/^[A-Z0-9]{3,20}$/')]
        public string $sku,
    ) {}
}

// [x] Output: serializer groups prevent data leakage
// Never return an entity directly, always use serializer groups
// or dedicated response DTOs

// [x] CORS: allowed origins only
// nelmio/cors-bundle: allow_origin: ['https://mironsoft.de']

// [x] Rate limiting on all writing endpoints (see RateLimitSubscriber)

3. Error handling checklist: formats and completeness

Error responses are the most commonly neglected dimension of an API review. It is common for reviewers to check the happy path thoroughly and only skim error responses. Yet error responses matter just as much as success responses for most consumers: frontend teams need to display errors meaningfully, monitoring systems need to distinguish error types, and retry logic needs to decide, based on the error status, whether a retry makes sense.

The error handling checklist checks three dimensions. First the format (RFC 7807 with the correct content type). Second the completeness (all documented status codes are actually triggered). Third the information density (enough information for the client to react, but no internal stack traces, database details or system paths that leak security information).

4. Performance checklist: data volume and queries

Performance problems in APIs often only become visible in production, because local test data is too small to expose N+1 problems or offset pagination degradation. A performance checklist in the review helps catch these problems proactively before they reach production. The Symfony Profiler is the most important tool here: it shows the number of database queries per request and makes N+1 problems immediately visible.

# Performance Checklist - run for every list endpoint and new database query

## Database Queries
[ ] Symfony Profiler: number of queries per request checked?
[ ] No N+1: relationships loaded via JOIN or eager loading?
[ ] Index present for all WHERE conditions in list queries?
[ ] No SELECT * (no unnecessary fields being loaded)?
[ ] Pagination: cursor-based for > 10,000 expected entries?

## Data Volume
[ ] Response size measured for typical list queries?
[ ] Sparse fieldsets implemented for endpoints with many fields?
[ ] Embedded resources optional (via ?embed=) instead of always loaded?
[ ] Binary data (images, PDFs) not embedded as Base64 in JSON?

## Caching
[ ] GET endpoints: Cache-Control header set?
[ ] ETag implemented for rarely changing resources?
[ ] Vary: Accept-Encoding set when compression is active?
[ ] Redis cache present for expensive aggregations?

## Network
[ ] HTTP compression configured in Nginx (gzip/brotli)?
[ ] Large lists offered as a streaming endpoint or with pagination?
[ ] HTTP/2 or HTTP/3 configured at the load balancer?

5. Documentation checklist: OpenAPI completeness

A new endpoint without complete OpenAPI documentation is a debt that grows immediately: frontend developers will document it informally (Notion, Slack, comments), the unofficial documentation drifts from the code, and the team loses its single source of truth. The documentation checklist in the review ensures that no endpoint gets merged before the OpenAPI specification is complete, including all error responses, examples and security requirements.

Spectral as an automated linter checks a large part of OpenAPI completeness mechanically: required fields, naming conventions and error format requirements. What Spectral does not check: semantic correctness (do the examples match the implementation?) and completeness of the descriptions (are the fields documented in a human-understandable way?). These aspects remain part of the manual review.

# OpenAPI Documentation Completeness Checklist
# Every new endpoint must have all these elements before merge

paths:
  /products/{id}:
    get:
      # [ ] operationId: camelCase, unique, descriptive
      operationId: getProduct

      # [ ] summary: short and precise
      summary: "Retrieve a single product by ID"

      # [ ] description: when to use it? what to watch out for?
      description: |
        Returns the full product representation including pricing and stock.
        Requires 'products:read' scope. Cached for 5 minutes.

      # [ ] tags: at least one tag for grouping
      tags: [Products]

      # [ ] parameters: all path, query and header parameters documented
      parameters:
        - name: id
          in: path
          required: true
          description: "Numeric product ID (positive integer)"
          schema:
            type: integer
            minimum: 1
            example: 42

      # [ ] responses: ALL possible status codes documented
      responses:
        "200":
          description: "Product found"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Product"
              # [ ] example present
              example:
                id: 42
                name: "Mironsoft T-Shirt"
                price: 29.99

        # [ ] 401 and 403 if secured
        "401":
          $ref: "#/components/responses/Unauthorized"

        # [ ] 404 if the resource does not always exist
        "404":
          $ref: "#/components/responses/NotFound"

      # [ ] security: declared (or security: [] if public)
      security:
        - BearerAuth: ["products:read"]

6. Test checklist: coverage and scenarios

API tests in Symfony projects typically follow three layers: unit tests for individual services and validators, integration tests for controllers (with a real HTTP stack), and contract tests that validate responses against the OpenAPI schema. A test checklist in the review checks whether all three layers are covered and whether the relevant test scenarios are present. A common finding in review: only the happy path is tested, and all error paths (401, 403, 404, 422) are missing.

Particularly important: authorization tests that check whether user A can access resources belonging to user B. These tests are often forgotten because they require using two different authenticated users in the test. In the Symfony context: two WebTestCase clients with different JWT tokens, a request to a resource belonging to the other user, expected status code 403. These tests catch IDOR vulnerabilities before they reach production.

request('GET', '/api/v2/orders/1', [], [], [
            'HTTP_AUTHORIZATION' => 'Bearer ' . $this->getTokenForUser('user_1'),
        ]);
        self::assertResponseStatusCodeSame(200);
        self::assertResponseHeaderSame('Content-Type', 'application/json');
    }

    // [ ] Auth test: no token -> 401
    public function testUnauthenticatedRequestReturns401(): void
    {
        $client = static::createClient();
        $client->request('GET', '/api/v2/orders/1');
        self::assertResponseStatusCodeSame(401);
        self::assertResponseHeaderSame('Content-Type', 'application/problem+json');
    }

    // [ ] Authorization test: other user's order -> 403 (IDOR prevention)
    public function testOtherUserCannotReadOrder(): void
    {
        $client = static::createClient();
        $client->request('GET', '/api/v2/orders/1', [], [], [
            'HTTP_AUTHORIZATION' => 'Bearer ' . $this->getTokenForUser('user_2'),
        ]);
        self::assertResponseStatusCodeSame(403);
    }

    // [ ] Not found: non-existent ID -> 404 with ProblemDetails
    public function testNonExistentOrderReturns404(): void
    {
        $client = static::createClient();
        $client->request('GET', '/api/v2/orders/99999', [], [], [
            'HTTP_AUTHORIZATION' => 'Bearer ' . $this->getTokenForUser('user_1'),
        ]);
        self::assertResponseStatusCodeSame(404);
        $body = json_decode($client->getResponse()->getContent(), true);
        self::assertArrayHasKey('type', $body);
        self::assertArrayHasKey('status', $body);
        self::assertSame(404, $body['status']);
    }

    // [ ] Validation: missing required fields -> 422 with violations
    public function testCreateOrderWithMissingFieldsReturns422(): void
    {
        $client = static::createClient();
        $client->request('POST', '/api/v2/orders', [], [], [
            'HTTP_AUTHORIZATION' => 'Bearer ' . $this->getTokenForUser('user_1'),
            'CONTENT_TYPE' => 'application/json',
        ], '{}');
        self::assertResponseStatusCodeSame(422);
        $body = json_decode($client->getResponse()->getContent(), true);
        self::assertArrayHasKey('violations', $body);
    }

    // [ ] Rate limit: exceeded -> 429 with Retry-After
    public function testRateLimitExceededReturns429(): void
    {
        // Implementation-dependent: test with in-memory limiter set to limit 1
        $this->markTestIncomplete('Rate limit test requires test-specific limiter config');
    }

    private function getTokenForUser(string $userId): string
    {
        return $_ENV['TEST_TOKEN_' . strtoupper($userId)] ?? 'test-token';
    }
}

7. Deployment checklist: backward compatibility and migration

Deployment checklists for APIs address questions that are often overlooked in code review because they go beyond the code itself: is this change backward compatible? Are there database migrations that must be atomic with the code deployment? Do consumers need to be notified? These questions are especially relevant for teams running public or semi-public APIs, where consumers cannot be updated at the same time as the server code.

Breaking changes are automatically detected by oasdiff when the tool runs in the CI pipeline. What it does not detect: semantic breaking changes, where the signature stays the same but the behavior changes. An endpoint that previously returned paginated results under a data key and now returns a plain array directly is a breaking change that triggers no schema diff. These semantic changes must be identified and documented manually during review.

8. Review categories compared: frequency and impact

Not all checklist categories have the same impact and the same frequency in practice. Experience from Symfony API reviews shows which categories most often produce findings and which have the biggest impact on API quality.

Review Category Frequency of Findings Impact if Missed Automatable?
Security / Authorization Medium Very high (data leak) Partially (static analysis)
Error Formats Very high Medium (client complexity) Yes (contract tests)
Design / Status Codes High High (breaking change) Partially (Spectral)
Performance / N+1 Medium High (production outage) No (manual needed)
Tests / Coverage High Medium (later bugs) Yes (coverage reports)

9. Summary

A systematic API review checklist for Symfony teams makes reviews faster, more complete and reproducible. The seven categories, design, security, error handling, performance, documentation, tests and deployment, cover all the relevant dimensions. Each category has a clear checklist structure that guides even inexperienced reviewers through the most important checkpoints. Automatable checks (Spectral, contract tests, coverage reports) get integrated into the CI pipeline and no longer need to be checked manually during review.

The most important aspect of a checklist is consistent application. A checklist that gets skipped in review whenever time pressure builds up provides no value. Teams that anchor the checklist as a fixed part of the PR template (GitHub/GitLab PR template with a checkbox list) ensure that every PR has explicitly addressed the checklist, and that the reviewer can trace which points the author checked themselves.

REST API Review Checklist: the essentials at a glance

Automatable

Spectral for OpenAPI linting. Contract tests for schema conformance. oasdiff for breaking changes. Coverage reports for test coverage. Integrate into CI.

Check manually

Authorization/ownership (IDOR). Semantic breaking changes. N+1 problems via profiler. Information density in error messages.

Highest impact

Security (authorization gaps). Design mistakes (lead to breaking changes). Performance problems (production outages). Always check first.

PR template

Anchor the checklist as a checkbox list in the PR template. Author fills it out, reviewer validates. Makes reviews traceable and structured.

10. FAQ: REST API Review Checklist for Symfony Teams

1How long does an API review with a checklist take?
With CI automation (Spectral, contract tests): 15 to 30 min manual effort per endpoint. Without automation: 45 to 60 min. The CI investment pays off quickly.
2What is IDOR and how is it detected?
Access to someone else's resource without an ownership check. In review: is there no security voter or getUserId() check? In a test: user B accesses user A's resource, 403 must come back.
3Which tests are mandatory per endpoint?
200/201 (happy path), 401 (no token), 403 (foreign resource), 404 (non-existent), 422 (validation). Optional: 409, 429, contract test.
4What does oasdiff check?
Removed endpoints, fields, enum values, changed types, new required fields. Semantic changes with the same signature are not detected.
5How to integrate the checklist into the PR process?
PR template with a checkbox list. Author fills it in, reviewer validates. CI blocks on Spectral/contract test failures. Makes reviews traceable.
6Most important security check?
Authorization at the resource level (IDOR). OWASP Top 10. Many teams only check auth, not whether a user can access someone else's resource.
7How to spot N+1 in code review?
A loop with a relationship access per iteration. In the profiler: query count is proportional to result size. Fix: eager loading with JOIN.
8What belongs in an OpenAPI description?
Required: operationId, summary, tags, all status codes, security. Recommended: description, examples, parameter descriptions with constraints.
9Separate design review from code review?
Design first: review the YAML before implementation. If not possible: always check design points first, before code quality is assessed.
10What is a semantic breaking change?
A behavior change with an unchanged OpenAPI signature. oasdiff does not detect it. Examples: the date format changes, a filter interprets values differently, a field now returns null.