Define, Generate, and Implement: An API-First Approach with OpenAPI Generator and FlightPHP

Adopting an API-first strategy ensures that your server and client remain in sync, dramatically reducing integration issues. By defining your API contract upfront, you can automatically generate both server stubs and client SDKs. This not only minimizes manual work but also creates a "typesafe" bridge between your front-end and back-end -- something even PHP developers can appreciate ;-) Defining Your API Begin by describing your API in an OpenAPI specification (e.g., my_api.yaml). In your spec, define endpoints, request/response schemas, and authentication details. For instance, a simple user endpoint might be defined as follows: openapi: 3.0.0 info: title: Example API version: 1.0.0 paths: /users/{id}: get: summary: Retrieve a user by ID parameters: - name: id in: path required: true schema: type: integer responses: '200': description: A user object content: application/json: schema: $ref: '#/components/schemas/User' put: summary: Update a user by ID parameters: - name: id in: path required: true schema: type: integer requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/User' responses: '204': description: User updated components: schemas: UserState: type: string enum: - active - disabled - pending User: type: object required: - id - state properties: id: type: integer name: type: string email: type: string state: $ref: '#/components/schemas/UserState' While this might seem bloated at first glance, bear with me — this approach makes subsequent implementations much easier and more robust! Generating the Server Stub and Client SDK With your API defined, use the OpenAPI Generator to generate your code automatically. The PHP Flight generator — documented here — was provided by the author and, although its status is still marked as "experimental", it has been my production workhorse for over a year. Here’s an example shell script that automates the generation of a FlightPHP server stub: #!/bin/bash set -e OPEN_API_GEN_VERSION="7.9.0" GENERATOR_JAR="openapi-generator-cli-${OPEN_API_GEN_VERSION}.jar" # Download the generator if needed: if [ ! -f "$GENERATOR_JAR" ]; then curl -o "$GENERATOR_JAR" "https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${OPEN_API_GEN_VERSION}/openapi-generator-cli-${OPEN_API_GEN_VERSION}.jar" fi # Clean previous outputs rm -rf generated-server # Generate a FlightPHP server stub using the php-flight generator java -jar "$GENERATOR_JAR" generate -i my_api.yaml -g php-flight -o generated-server --additional-properties=invokerPackage=GeneratedApi,composerPackageName=myapi/server Of course, you can similarly generate your API (frontend) clients. This automated process ensures that any changes to your API spec are consistently propagated across all layers, keeping your integration robust and typesafe. You can choose whether to track these generated files in version control or simply re-generate them in your CI build. Check Out the Generated Source The generated files form a complete composer package, that you could package and use as dependency. You can as well keep it in a subfolder and include it as a local composer repository. ├── Api │   └── AbstractDefaultApi.php ├── Model │   ├── User.php │   └── UserState.php ├── README.md ├── RegisterRoutes.php ├── Test │   └── RegisterRoutesTest.php ├── composer.json └── phpunit.xml.dist The User model, for example, looks like this (without comments for better readability):

Feb 24, 2025 - 11:46
 0
Define, Generate, and Implement: An API-First Approach with OpenAPI Generator and FlightPHP

Adopting an API-first strategy ensures that your server and client remain in sync, dramatically reducing integration issues. By defining your API contract upfront, you can automatically generate both server stubs and client SDKs. This not only minimizes manual work but also creates a "typesafe" bridge between your front-end and back-end -- something even PHP developers can appreciate ;-)

Defining Your API

Begin by describing your API in an OpenAPI specification (e.g., my_api.yaml). In your spec, define endpoints, request/response schemas, and authentication details. For instance, a simple user endpoint might be defined as follows:

openapi: 3.0.0
info:
  title: Example API
  version: 1.0.0
paths:
  /users/{id}:
    get:
      summary: Retrieve a user by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: A user object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
    put:
      summary: Update a user by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'
      responses:
        '204':
          description: User updated
components:
  schemas:
    UserState:
      type: string
      enum:
        - active
        - disabled
        - pending
    User:
      type: object
      required:
        - id
        - state
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
        state:
          $ref: '#/components/schemas/UserState'

While this might seem bloated at first glance, bear with me — this approach makes subsequent implementations much easier and more robust!

Generating the Server Stub and Client SDK

With your API defined, use the OpenAPI Generator to generate your code automatically. The PHP Flight generator — documented here — was provided by the author and, although its status is still marked as "experimental", it has been my production workhorse for over a year.

Here’s an example shell script that automates the generation of a FlightPHP server stub:

#!/bin/bash
set -e

OPEN_API_GEN_VERSION="7.9.0"
GENERATOR_JAR="openapi-generator-cli-${OPEN_API_GEN_VERSION}.jar"

# Download the generator if needed:
if [ ! -f "$GENERATOR_JAR" ]; then
    curl -o "$GENERATOR_JAR" "https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${OPEN_API_GEN_VERSION}/openapi-generator-cli-${OPEN_API_GEN_VERSION}.jar"
fi

# Clean previous outputs
rm -rf generated-server

# Generate a FlightPHP server stub using the php-flight generator
java -jar "$GENERATOR_JAR" generate -i my_api.yaml -g php-flight -o generated-server --additional-properties=invokerPackage=GeneratedApi,composerPackageName=myapi/server

Of course, you can similarly generate your API (frontend) clients. This automated process ensures that any changes to your API spec are consistently propagated across all layers, keeping your integration robust and typesafe. You can choose whether to track these generated files in version control or simply re-generate them in your CI build.

Check Out the Generated Source

The generated files form a complete composer package, that you could package and use as dependency. You can as well keep it in a subfolder and include it as a local composer repository.

├── Api
│   └── AbstractDefaultApi.php
├── Model
│   ├── User.php
│   └── UserState.php
├── README.md
├── RegisterRoutes.php
├── Test
│   └── RegisterRoutesTest.php
├── composer.json
└── phpunit.xml.dist

The User model, for example, looks like this (without comments for better readability):



namespace GeneratedApi\Model;

class User implements \JsonSerializable
{
    public int $id;
    public ?string $name;
    public ?string $email;
    public UserState $state;

    public function __construct(int $id, ?string $name, ?string $email, UserState $state)
    {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
        $this->state = $state;
    }

    public static function fromArray(array $data): self
    {
        return new self(
            $data['id'] ?? null, 
            $data['name'] ?? null, 
            $data['email'] ?? null, 
            isset($data['state']) ? UserState::tryFrom($data['state']) : null, 
        );
    }

    public function jsonSerialize(): mixed {
        return [
            'id' => $this->id, 
            'name' => $this->name, 
            'email' => $this->email, 
            'state' => $this->state, 
        ];
    }
}

Implementing Your API Endpoints

Once your server stub is generated, extend the generated abstract API classes to implement your business logic. An example implementation for the User API might look like this:



namespace MyApp\Api;

use GeneratedApi\Api\AbstractDefaultApi;
use GeneratedApi\Model\User;
use GeneratedApi\Model\UserState;
use Flight;

class UserApi extends AbstractDefaultApi
{
    // Simulate fetching a user from a data source
    public function usersIdGet(int $id): ?User {
        if ($id === 1) {
            return new User(1, 'Alice Smith', 'alice@example.com', UserState::PENDING);
        }
        Flight::halt(404, "User not found");
        return null;
    }


    public function usersIdPut(int $id, User $user): void {
         // store user in DB - might use $user->jsonSerialize() for serialization
    }
}

After implementing your API logic, register your endpoints with FlightPHP to integrate them into your application routing:



// In your Flight bootstrap file
use MyApp\Api\UserApi;
use GeneratedApi\RegisterRoutes;

RegisterRoutes::registerRoutes(new UserApi());
Flight::start();

This ties your custom implementation to FlightPHP’s routing system, ensuring that your API behaves as defined.

Note all the things you don't need to do with this approach:

  • No manual de-/serialization needed (you get your parameters in correct type - e.g. userId as int!)
  • No manual checking if response/request is as expected (conforms to schema)
  • No manual set up of paths/routes

Final Thoughts

By following an API-first approach with OpenAPI Generator and FlightPHP, you ensure a consistent, typesafe contract between your front-end and back-end. While the initial setup might take a little effort, it quickly pays off during subsequent iterations. This strategy treats your API as first-class citizen and helps you maintaining this API with any upcoming changes!

I’d love to hear your thoughts or experiences with this approach. Please share your feedback or any questions you might have.