Skip to content

The Category Trash Can: One Endpoint and POST Restore

Adityo Guni Waluyo

A soft delete API contract: the trashed query flag, POST restore, and a 404 that deliberately merges two conditions.

TL;DR

The categories list supports a trashed query param defaulting to false so existing clients keep seeing active items while new UIs can fetch soft-deleted ones. Restore is a POST endpoint returning 200 on success, 403 when out of scope, or a shared 404 for missing or not-trashed cases. It's a clean additive contract-first change that keeps compatibility without new endpoints.

I was reviewing an OpenAPI diff for the KotaPortal admin module when a change to the category schema showed up. Not just another field: a whole new soft delete mechanism.

Honestly, my initial guess was the standard one. When you build a trash can feature, you reach for a separate endpoint like GET /admin/categories/trash. And for restore, maybe a DELETE on that trash resource, or a PATCH flipping the status back to active.

The diff did neither. Instead of a new endpoint, the contract added one boolean query parameter named trashed to the admin category list endpoint, defaulting to false. False answers the active list as usual. True answers only the soft-deleted categories.

This is an additive change. In an ecosystem like KotaPortal, older mobile app versions or legacy admin panels may still call the category endpoint without the new parameter. A separate endpoint, or a changed default behavior, breaks compatibility on the spot. With false as the default, the contract stays safe: old consumers keep getting the data they expect, while updated frontends can use the new feature immediately.

For restore, the new endpoint is POST /admin/categories/{id}/restore. Only three responses are documented: 200 with a small object carrying an ok flag, 403 when the category is outside the requester's scope, and a 404 with a very specific description: "Category not found (or not trashed)".

paths:
  /admin/categories:
    get:
      parameters:
        - name: trashed
          in: query
          schema:
            type: boolean
            default: false
  /admin/categories/{id}/restore:
    post:
      responses:
        '200':
          description: Restore result
        '404':
          description: Category not found (or not trashed)
        '403':
          description: Out of scope

Why the 404 Merges Two Conditions

Because restore only succeeds when the category is actually in the trash. A category that does not exist at all and a category that exists but is not trashed both lack a valid representation to restore. RFC 9110 says it plainly: POST processes a representation according to the resource's own semantics, and 404 means the server did not find a current representation (or is not willing to disclose one), without saying whether that condition is temporary or permanent [5].

The Query Parameter Through the Spec's Eyes

In the OpenAPI specification, the Parameter object has a field picking its location: query, header, path, or cookie. Path parameters must carry the required marker, while everything else defaults to optional [6]. That makes the trashed parameter optional with a safe default. The same principle appears in API conventions: optional fields must not be assumed present by the reader, and defaults belong on optional fields [3].

Contract-First Means the Spec Diff Is the Client Diff

The side effect I like most: the type definition file generated by openapi-typescript picks up the new parameter and the restore operation automatically. No hand-writing, no checking one by one.

The same diff carries the entity schema additions: contact_person turned nullable, plus rating_enabled, rental_enabled, and type_id reserved for a future master table. All of it follows the same pattern. We do not delete or retype anything old; we only add what is needed.

I was skeptical about that 404 description merging two conditions. After sitting with it, this is a solid contract decision. No complicated state machine just to say "hey, this one is not trashed anymore". Reject with a 404 and let the client handle the UI logic.

## Sources [3] https://github.com/golang-migrate/migrate/blob/master/MIGRATIONS.md [5] https://datatracker.ietf.org/doc/html/rfc9110 [6] https://spec.openapis.org/oas/v3.1.1

Related articles