Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Design a REST API with OpenAPI and Swagger

By Kokil Thapa | Last reviewed: September 2026

You need a REST API that mobile apps, partner systems, and your own frontend can rely on for years. The fastest way to get there is to design a REST API with OpenAPI and Swagger before you write route handlers, not after. OpenAPI 3.1 gives you a machine-readable contract. Swagger UI turns that contract into interactive docs your team can test on day one. On production Laravel applications I've shipped since 2010, this contract-first approach has prevented the drift that kills integrations. This guide walks through resource modelling, spec authoring, validation, and deployment using patterns from real API development projects.

What is the difference between OpenAPI and Swagger when you design a REST API?

OpenAPI is the specification format. Swagger is the tooling ecosystem built around it. Think of OpenAPI as the blueprint and Swagger as the workshop where you preview and test that blueprint.

Before 2016, "Swagger" referred to both the spec and the tools. The spec was donated to the Linux Foundation and renamed OpenAPI. Today, Swagger UI, Swagger Editor, and Swagger Codegen remain the most widely used interfaces for working with OpenAPI documents.

TermWhat it isWhen you use it
OpenAPI 3.1JSON/YAML specification standardDefining paths, schemas, security, examples
Swagger UIBrowser-based interactive docsManual testing, partner onboarding
Swagger EditorOnline or self-hosted spec editorDrafting and linting specs
RedocAlternative doc rendererPublic developer portals with read-only docs

For deeper spec syntax, see the OpenAPI 3.1 specification guide. For choosing between Swagger UI and Redoc in production, read API documentation with Redoc and Swagger UI.

Contract-First REST API PipelineDomain ModelResources + verbsOpenAPI 3.1YAML contractSwagger UITry-it-out docsLaravel 13Routes + testsCI Pipeline Keeps Spec and Code AlignedSpectral lintContract testsDeploy docsFail the build when responses drift from the published contract
Contract-first workflow to design a REST API with OpenAPI and Swagger before Laravel route implementation

How do you model REST resources before writing an OpenAPI spec?

Start with nouns, not endpoints. A booking portal needs bookings, clients, and documents. Each resource gets a plural URI segment and standard HTTP verbs mapped to intent.

Map HTTP verbs to business actions

  • GET /bookings — list with pagination and filters
  • GET /bookings/{id} — fetch one record
  • POST /bookings — create; return 201 with Location header
  • PUT /bookings/{id} — full replace
  • PATCH /bookings/{id} — partial update
  • DELETE /bookings/{id} — remove or soft-delete

Follow the same resource rules outlined in REST API design best practices for 2026. Avoid verbs in URLs like /createBooking. Use query parameters for filtering, not new path segments.

Version your API in the path

Prefix every route with /api/v1/. When breaking changes arrive, ship /api/v2/ and keep v1 running through a sunset window. I've used this pattern on client portals where mobile apps cannot force-update overnight. See Laravel API versioning strategy for migration tactics.

How do you write an OpenAPI 3.1 specification for a REST API?

Place your spec at openapi/openapi.yaml in the repo root. Use YAML for readability. The official OpenAPI 3.1 specification is the authoritative reference for every field.

Minimal working spec for a bookings API

openapi: 3.1.0
info:
  title: Client Portal Bookings API
  version: 1.0.0
  description: REST API for appointment booking and document uploads.
servers:
  - url: https://api.example.com/api/v1
    description: Production
  - url: http://localhost:8000/api/v1
    description: Local (Laravel 13 + PHP 8.3+)
paths:
  /bookings:
    get:
      summary: List bookings
      operationId: listBookings
      tags: [Bookings]
      parameters:
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
        - name: status
          in: query
          schema: { type: string, enum: [pending, confirmed, cancelled] }
      responses:
        '200':
          description: Paginated booking list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BookingCollection'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      summary: Create a booking
      operationId: createBooking
      tags: [Bookings]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BookingCreate'
      responses:
        '201':
          description: Booking created
          headers:
            Location:
              schema: { type: string, format: uri }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Booking'
        '422':
          $ref: '#/components/responses/ValidationError'
  /bookings/{bookingId}:
    get:
      summary: Get one booking
      operationId: getBooking
      tags: [Bookings]
      parameters:
        - $ref: '#/components/parameters/BookingId'
      responses:
        '200':
          description: Single booking
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Booking'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  parameters:
    BookingId:
      name: bookingId
      in: path
      required: true
      schema: { type: string, format: uuid }
  schemas:
    Booking:
      type: object
      required: [id, client_name, scheduled_at, status]
      properties:
        id: { type: string, format: uuid }
        client_name: { type: string, maxLength: 120 }
        scheduled_at: { type: string, format: date-time }
        status: { type: string, enum: [pending, confirmed, cancelled] }
    BookingCreate:
      type: object
      required: [client_name, scheduled_at]
      properties:
        client_name: { type: string, maxLength: 120 }
        scheduled_at: { type: string, format: date-time }
    BookingCollection:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/Booking' }
        meta:
          type: object
          properties:
            current_page: { type: integer }
            last_page: { type: integer }
            total: { type: integer }
    Error:
      type: object
      required: [message]
      properties:
        message: { type: string }
        errors:
          type: object
          additionalProperties:
            type: array
            items: { type: string }
  responses:
    Unauthorized:
      description: Missing or invalid token
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ValidationError:
      description: Validation failed
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
security:
  - bearerAuth: []

Paste the YAML into the JSON formatter when you need to convert between YAML and JSON for CI tools. Keep schemas in components/schemas and reference them with $ref to avoid duplication.

OpenAPI 3.1 Document Structureopenapi: 3.1.0 | info | servers | securitypaths/bookings/bookings/{id}get, post, patch, deleteparameters, requestBody, responsesoperationId + tags per endpointcomponentsschemasresponsesparameterssecuritySchemes$ref
Core sections of an OpenAPI 3.1 file used to design REST API endpoints and reusable schemas

How do you serve Swagger UI and validate requests against the OpenAPI contract?

Swagger UI renders your spec as browsable docs with a Try-it-out button. Request validation ensures incoming traffic matches the contract before your controller runs. Both steps close the gap between documentation and behaviour.

Install Swagger UI in a Laravel 13 project

Laravel 13 requires PHP 8.3 or higher. Laravel 12 runs on PHP 8.2+. Add a lightweight package or serve static Swagger UI assets from public/docs.

  1. Copy the official Swagger documentation bundle into public/swagger-ui/.
  2. Create resources/views/docs/swagger.blade.php that loads Swagger UI and points at /openapi.yaml.
  3. Expose the spec via a read-only route or symlink from public/openapi.yaml.
  4. Protect staging docs with HTTP basic auth. Keep production docs public only when the API is partner-facing.
<!-- resources/views/docs/swagger.blade.php -->
<link rel="stylesheet" href="/swagger-ui/swagger-ui.css">
<div id="swagger-ui"></div>
<script src="/swagger-ui/swagger-ui-bundle.js"></script>
<script>
  SwaggerUIBundle({
    url: '/openapi.yaml',
    dom_id: '#swagger-ui',
    deepLinking: true,
    presets: [SwaggerUIBundle.presets.apis],
  });
</script>

Validate requests with middleware

Use league/openapi-psr7-validator or generate Laravel Form Request classes from the spec. On a legal-tech portal I built, OpenAPI-driven validation caught malformed document-upload payloads before they hit storage logic.

composer require league/openapi-psr7-validator

/* app/Http/Middleware/ValidateOpenApi.php */
public function handle(Request $request, Closure $next)
{
    $validator = new ValidatorBuilder()
        ->fromYamlFile(base_path('openapi/openapi.yaml'))
        ->getRequestValidator();

    $validator->validate(
        $request->method(),
        $request->getPathInfo(),
        (string) $request->getContent()
    );

    return $next($request);
}

Pair validation with Sanctum token auth as described in building a REST API with Laravel Sanctum authentication. Apply the middleware only to /api/* routes so web sessions stay untouched.

What security and error patterns belong in every OpenAPI REST API design?

A spec without security schemes is incomplete. Document auth, rate limits, and error envelopes in the same file your partners read.

Security schemes to declare

  • Bearer tokens — Sanctum or JWT for mobile and SPA clients
  • API keys — server-to-server integrations with scoped keys
  • OAuth 2.0 — third-party apps needing delegated access

Cross-check your design against the API security complete checklist. Never expose internal IDs sequentially if the resource is sensitive. UUIDs reduce enumeration risk on client portals.

Standardise error responses

Every 4xx and 5xx response should share one schema. Laravel's default validation JSON already fits the Error schema above. Document 401, 403, 404, 422, and 429 in components/responses so Swagger UI shows them per endpoint.

For write operations that partners may retry, add idempotency key support. The header belongs in your OpenAPI parameters section. Read API idempotency keys implementation guide for header naming and storage patterns.

Request Lifecycle with OpenAPI ValidationClientPOST JSONAuthSanctumOpenAPIValidateControllerBusinessJSON201422 RejectSpec mismatchInvalid payloads never reach domain logic
OpenAPI request validation middleware blocks malformed traffic before Laravel controllers execute business rules

How do you keep OpenAPI specs and Laravel code in sync during CI/CD?

Specs drift unless CI enforces them. Treat the OpenAPI file as source code reviewed in every pull request. This is the core of API-first development workflow.

Lint the spec on every commit

npm install -g @stoplight/spectral-cli

spectral lint openapi/openapi.yaml --ruleset .spectral.yaml

Add rules that require operationId, response examples, and description fields. Spectral catches incomplete endpoints before merge.

Run contract tests against a running app

Spin up Laravel in CI, hit each documented path, and compare responses to the schema. Tools like Dredd or Schemathesis read your OpenAPI file and fuzz inputs automatically. For consumer-driven checks, see API contract testing with Pact.

Wire Laravel routes to match the spec

/* routes/api.php — Laravel 13 */
Route::prefix('v1')->middleware(['auth:sanctum', 'openapi.validate'])->group(function () {
    Route::get('/bookings', [BookingController::class, 'index']);
    Route::post('/bookings', [BookingController::class, 'store']);
    Route::get('/bookings/{booking}', [BookingController::class, 'show']);
    Route::patch('/bookings/{booking}', [BookingController::class, 'update']);
    Route::delete('/bookings/{booking}', [BookingController::class, 'destroy']);
});

Follow Laravel routing documentation for route model binding and middleware groups. Align controller return types with your OpenAPI response schemas. Use API Resources or dedicated DTO serializers so JSON shape stays predictable.

On projects using GitLab CI and Deployer 7, I publish the spec artifact alongside each release. Partners always see docs that match the deployed build. The same pipeline pattern runs on sister legal-tech sites sharing shared EC2 infrastructure.

Code-First vs Contract-First OutcomesCode-FirstDocs written after shippingSwagger annotations in controllersSpec drifts within weeksPartners hit surprise 422sMobile releases blockedHigher support costContract-FirstOpenAPI spec leads designSwagger UI from day oneCI blocks spec driftParallel frontend workGenerated SDKs optionalFaster partner onboardingDesign a REST API with OpenAPI and Swagger before controllers
Contract-first OpenAPI design reduces documentation drift compared to annotating controllers after launch

Generate client SDKs when partners need them

OpenAPI Generator can produce PHP, JavaScript, or mobile client stubs from the same YAML file. If you publish a public API, document SDK usage in a separate guide. See SDK design for your public API for naming and versioning rules.

For webhook callbacks documented alongside REST endpoints, apply the same schema discipline. Event payloads deserve their own schemas under components. Read webhook design patterns for reliability before adding POST /webhooks routes.

A portal like Mijar Law Associates combines REST endpoints for bookings, document uploads, and payment status. Each integration point started as an OpenAPI path review with the client's IT team before a line of PHP shipped.

Key Takeaways

  • Write the OpenAPI 3.1 YAML spec before Laravel routes; treat it as the single source of truth for paths, schemas, and errors.
  • Serve Swagger UI from a dedicated docs route and protect staging environments with basic auth.
  • Validate every API request against the spec in middleware so malformed JSON never reaches business logic.
  • Version paths with /api/v1/, document security schemes, and standardise error envelopes across all endpoints.
  • Run Spectral lint and contract tests in CI to block merges when code and spec diverge.
  • Generate SDKs from the same spec when partners need typed clients instead of raw HTTP calls.

People Also Ask

Can you use OpenAPI with Laravel without Swagger annotations?

Yes. A standalone openapi.yaml file is often cleaner than scattering annotations in controllers. You load the file into Swagger UI and validate requests with middleware. Annotations work for code-first teams, but they couple documentation to implementation details.

Should new REST APIs use OpenAPI 3.0 or 3.1?

Start new projects on OpenAPI 3.1. It aligns JSON Schema draft 2020-12 and handles nullable types more cleanly. Most Swagger UI versions released after 2023 support 3.1 without issues.

How do you document pagination in OpenAPI?

Define a reusable meta object in components/schemas with current_page, last_page, and total fields. Reference it in list endpoint responses. Document query parameters page and per_page on each collection GET operation.

Is Swagger UI safe to expose in production?

Public partner APIs benefit from public docs. Internal admin APIs should sit behind authentication or VPN access. Never expose Try-it-out against production data without rate limiting and scoped tokens.

Ship APIs your partners can trust

The teams that integrate fastest are the ones who receive a tested OpenAPI file on day one, not a PDF emailed after launch. When you design a REST API with OpenAPI and Swagger as the contract layer, documentation, validation, and client SDKs stay aligned through every release. Start with resource modelling, commit the YAML spec, wire Swagger UI, and enforce the contract in CI.

Need help designing a production API for a portal, eCommerce backend, or mobile companion app? Review how to build a REST API in Laravel the right way and Laravel API best practices, then contact us for architecture and implementation support through custom software development.

Frequently Asked Questions

OpenAPI is the JSON/YAML specification standard for defining paths, schemas, and security. Swagger is the tooling ecosystem—Swagger UI, Swagger Editor, and Swagger Codegen—for previewing and testing that blueprint.

Model resources and HTTP verbs first, write an OpenAPI 3.1 YAML spec, generate Swagger UI docs, then validate every request against the contract before route handlers run.

Start new projects on OpenAPI 3.1. It aligns with JSON Schema draft 2020-12 and handles nullable types more cleanly. Most Swagger UI versions released after 2023 support 3.1 without issues.

Start with nouns, not endpoints. A booking portal needs bookings, clients, and documents. Each resource gets a plural URI segment mapped to HTTP verbs: GET for list and fetch, POST for create returning 201 with a Location header, PUT for full replace, PATCH for partial update, DELETE for remove or soft-delete. Avoid verbs in URLs like /createBooking. Use query parameters for filtering, not new path segments. Prefix every route with /api/v1/, and ship /api/v2/ with a sunset window when breaking changes arrive.

Place your spec at openapi/openapi.yaml in the repo root and use YAML for readability. Keep reusable schemas in components/schemas and reference them with $ref to avoid duplication. Expose the spec via a read-only route or a symlink from public/openapi.yaml so Swagger UI can load it. Treat the file as source code reviewed in every pull request, not something exported from controllers after implementation is done.

Start with openapi: 3.1.0, info, servers pointing at production and local /api/v1 URLs, then define paths with operationId, tags, parameters, requestBody, and responses. Centralise securitySchemes such as bearerAuth under components, along with shared parameters, schemas like Booking and BookingCreate, and standard responses for 401, 404, and 422. Apply security globally. Document write operations with required request bodies and correct status codes—201 for POST with a Location header, 200 for reads, 422 for validation failures matching Laravel's error envelope.

Laravel 13 requires PHP 8.3 or higher. Copy the official Swagger documentation bundle into public/swagger-ui/, then create resources/views/docs/swagger.blade.php loading Swagger UI CSS and JS with SwaggerUIBundle pointed at url: '/openapi.yaml'. Expose the spec at that path via symlink or route. Add a dedicated docs route serving the Blade view. Protect staging docs with HTTP basic auth. Keep production docs public only when the API is partner-facing and interactive Try-it-out onboarding is intentional.

Install league/openapi-psr7-validator via Composer. Create middleware such as ValidateOpenApi that loads openapi/openapi.yaml through ValidatorBuilder and validates the HTTP method, path, and request body before your controller runs. Register it as openapi.validate and apply it only to /api/* routes so web sessions stay untouched. Pair it with Sanctum token auth. On a legal-tech portal I built, this caught malformed document-upload payloads before they reached storage logic.

Yes. A standalone openapi.yaml file is often cleaner than scattering annotations in controllers. Load the file into Swagger UI and validate requests with middleware instead. Annotations suit code-first teams but couple documentation to implementation details. Contract-first design treats the YAML as the single source of truth for paths, schemas, and errors, which reduces the drift that kills integrations when docs are written after route handlers ship.

A spec without security schemes is incomplete. Document Bearer tokens for Sanctum or JWT clients, API keys for scoped server-to-server calls, and OAuth 2.0 for delegated third-party access. Use UUIDs instead of sequential IDs on sensitive resources to reduce enumeration risk. Standardise every 4xx and 5xx response under one Error schema with message and optional errors object. Document 401, 403, 404, 422, and 429 in components/responses so Swagger UI shows them per endpoint. For retriable write operations, add idempotency key parameters in the spec.

Define a reusable meta object in components/schemas with current_page, last_page, and total fields. Wrap list results in a collection schema containing a data array plus that meta object, as with BookingCollection. On each collection GET operation, document a page query parameter with type integer, minimum 1, and default 1. Add per_page where needed. This gives partners and Swagger UI Try-it-out a predictable paginated envelope that matches what your Laravel controllers return.

Lint openapi/openapi.yaml on every commit with @stoplight/spectral-cli and a .spectral.yaml ruleset requiring operationId, response examples, and descriptions. In CI, spin up Laravel and run contract tests with Dredd or Schemathesis, or consumer-driven checks with Pact. Wire routes/api.php to match the spec exactly under a v1 prefix with auth:sanctum and openapi.validate middleware. Align controller JSON with response schemas using API Resources or DTO serializers. On GitLab CI and Deployer 7 pipelines, publish the spec artifact with each release.

Public partner APIs benefit from browsable docs with Try-it-out for onboarding and manual testing. Protect staging environments with HTTP basic auth so incomplete specs are not publicly visible. Keep production Swagger UI public only when the API is intentionally partner-facing. Because docs reveal endpoint structure, request schemas, and auth patterns, treat public exposure as a deliberate product decision. Restrict internal APIs by authentication or network policy instead of leaving interactive docs open to the internet.

Write the OpenAPI 3.1 YAML spec before Laravel routes, treating it as the single source of truth. Model resources and HTTP intent first, define paths, schemas, security, and error envelopes, then implement controllers that conform. Serve Swagger UI from day one so your team and partners can test against the contract. Validate every request in middleware so malformed JSON never reaches business logic. I've used this on client portals where integration review happened before any PHP shipped, preventing the documentation drift that breaks mobile apps and partner systems.

OpenAPI Generator produces PHP, JavaScript, or mobile client stubs from the same YAML file that powers Swagger UI. Regenerate SDKs when the spec artifact ships with each release so typed clients match deployed behaviour. If you publish a public API, document SDK usage, naming, and versioning in a separate guide. Apply the same schema discipline to webhook event payloads under components/schemas alongside REST paths, so callbacks documented in the spec stay consistent with your REST endpoints.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: