http clients and apis

JSON APIs

JSON APIs use HTTP to exchange structured data. The client sends and receives JSON instead of HTML.

A good JSON API is predictable. Clients should know what status codes mean, what successful responses look like, what error responses look like, and which fields can be missing or null.

Return JSON with the right content type

PHP example
<?php

declare(strict_types=1);

$response = [
    'data' => [
        'id' => 123,
        'type' => 'product',
        'name' => 'Notebook',
    ],
];

echo json_encode($response, JSON_THROW_ON_ERROR) . PHP_EOL;

// Prints:
// {"data":{"id":123,"type":"product","name":"Notebook"}}

In a real HTTP response, also send Content-Type: application/json and the correct status code.

Choose a response shape

Many APIs wrap successful data in a data key and errors in an errors key. The exact convention matters less than consistency.

PHP example
<?php

declare(strict_types=1);

function productResponse(int $id, string $name): array
{
    return [
        'data' => [
            'id' => $id,
            'type' => 'product',
            'attributes' => [
                'name' => $name,
            ],
        ],
    ];
}

echo json_encode(productResponse(123, 'Notebook'), JSON_THROW_ON_ERROR) . PHP_EOL;

// Prints:
// {"data":{"id":123,"type":"product","attributes":{"name":"Notebook"}}}

Avoid returning completely different shapes for similar endpoints. Clients become fragile when one success response returns { "id": 1 } and another returns { "data": { "id": 1 } } without a reason.

Error responses

Error responses should be machine-readable and safe to show or log. Do not expose stack traces, SQL, secrets, or internal class names.

PHP example
<?php

declare(strict_types=1);

function errorResponse(string $code, string $message): array
{
    return [
        'errors' => [
            [
                'code' => $code,
                'message' => $message,
            ],
        ],
    ];
}

echo json_encode(errorResponse('not_found', 'Product not found.'), JSON_THROW_ON_ERROR) . PHP_EOL;

// Prints:
// {"errors":[{"code":"not_found","message":"Product not found."}]}

Pair the body with the correct status code, such as 404 for missing resources or 422 for validation errors.

Validation errors

Validation errors should point to the fields that failed.

PHP example
<?php

declare(strict_types=1);

$response = [
    'errors' => [
        ['field' => 'email', 'message' => 'Enter a valid email address.'],
        ['field' => 'name', 'message' => 'Name is required.'],
    ],
];

echo json_encode($response, JSON_THROW_ON_ERROR) . PHP_EOL;

// Prints:
// {"errors":[{"field":"email","message":"Enter a valid email address."},{"field":"name","message":"Name is required."}]}

Clients can use this shape to attach messages to form fields or show a useful API error.

Missing and null are different

In JSON, a field can be absent or present with null.

Absent often means "not included in this response". null often means "known to be empty".

Be deliberate. If middle_name is optional, returning "middle_name": null can be clearer than sometimes omitting it, but the important thing is documenting and staying consistent.

Lists should stay lists

For collections, return arrays consistently. An empty result should be [], not {} or null.

PHP example
<?php

declare(strict_types=1);

$response = ['data' => []];

echo json_encode($response, JSON_THROW_ON_ERROR) . PHP_EOL;

// Prints:
// {"data":[]}

What to check in a project

Check that JSON responses set Content-Type: application/json.

Check that success and error responses follow documented shapes.

Check that status codes match the outcome.

Check that validation errors are specific enough for clients to use.

Check that sensitive internals are not leaked in error bodies.

Check that empty lists, missing values, and null values are handled consistently.

What you should be able to do

After this lesson, you should be able to design basic JSON success and error responses, choose status codes that match outcomes, return validation errors cleanly, and explain why response consistency matters for API clients.

Practice

Task: Build JSON API Responses

Write a small PHP script that builds success and error response arrays for a JSON API.

Requirements

  • Use declare(strict_types=1);.
  • Create a success response with a data key.
  • Create an error response with an errors key.
  • Include a validation error with a field name.
  • Encode the responses with json_encode(..., JSON_THROW_ON_ERROR).
  • Print the encoded output.

Check Your Work

Run the script and confirm the success and error responses have different, predictable shapes.

Show solution

This solution keeps response shapes explicit and easy for a client to consume.

PHP example
<?php

declare(strict_types=1);

function productCreatedResponse(int $id, string $name): array
{
    return [
        'data' => [
            'id' => $id,
            'type' => 'product',
            'attributes' => [
                'name' => $name,
            ],
        ],
    ];
}

function validationErrorResponse(string $field, string $message): array
{
    return [
        'errors' => [
            [
                'field' => $field,
                'message' => $message,
            ],
        ],
    ];
}

echo json_encode(productCreatedResponse(123, 'Notebook'), JSON_THROW_ON_ERROR) . PHP_EOL;
echo json_encode(validationErrorResponse('name', 'Name is required.'), JSON_THROW_ON_ERROR) . PHP_EOL;

// Prints:
// {"data":{"id":123,"type":"product","attributes":{"name":"Notebook"}}}
// {"errors":[{"field":"name","message":"Name is required."}]}

In a real API response, the first body would likely use status 201 Created, while the validation body would use a 4xx status such as 422.

Why This Works

The success response has a data object. The validation failure has an errors list that tells the client which field failed and why.