← Back to the section

When you design a REST API, two questions come up quickly: how to address "yourself" without hardcoding your own ID, and how to express domain commands like "confirm the order" or "block the user" when a plain PATCH is not enough. Alias segments and action endpoints exist for exactly this.

The me alias — when you need it and when you don't

Imagine you have an endpoint GET /users/{id} that an administrator uses to view any user. A regular user also wants to see their profile — but why should they need to know their own ID? They just want to "view themselves".

The me alias solves this: GET /users/me reads as "get me", and the server pulls the ID from the token itself.

r.Route("/api/v1/users", func(r chi.Router) {
    r.Get("/{id}", getUser)    // GET /users/42 — administrator views anyone
    r.Get("/me", getMyUser)    // GET /users/me — user views themselves
    r.Put("/{id}", updateUser)
    r.Put("/me", updateMyUser)
})

The getMyUser handler does not take an ID from the URL — it takes it from the request context (JWT or session):

func getMyUser(w http.ResponseWriter, r *http.Request) {
    userID := mustUserID(r.Context())
    user, err := svc.GetUser(r.Context(), userID)
    if err != nil {
        httperr.Write(w, r, err)
        return
    }
    writeJSON(w, http.StatusOK, toUserResponse(user))
}

And getUser takes {id} and checks permissions — an administrator can view anyone:

func getUser(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")
    if !canViewUser(r.Context(), id) {
        httperr.Write(w, r, apperr.NewForbidden("access denied"))
        return
    }
    user, err := svc.GetUser(r.Context(), id)
    if err != nil {
        httperr.Write(w, r, err)
        return
    }
    writeJSON(w, http.StatusOK, toUserResponse(user))
}

The key rule: me is only needed when the same endpoint has an {id} version for the administrator. The test question: "Can a super-administrator access this resource by someone else's ID?" If yes — me is justified.

When me is not needed

If an endpoint only ever works with the caller's own resources, you don't need any me. Just /profile or /settings:

r.Get("/profile", getMyProfile)   // always about the current user from the token
r.Put("/settings", updateMySettings)

Adding /me to such endpoints is needless noise. And never make /api/v1/me without a resource prefix: the correct form is /api/v1/users/me, not /api/v1/me.

Temporal and ordinal aliases

Sometimes you need to get "the latest", "the current", or "the next" — not through a filter with sorting and a limit, but directly. That's what alias segments are for:

r.Get("/deployments/latest", getLatestDeployment)
r.Get("/subscriptions/current", getCurrentSubscription)
r.Get("/products/{id}/latest-review", getLatestReview)
r.Get("/invoices/next", getNextInvoice)

The handler is simple — the logic of selecting "the latest" is hidden in the service:

func getLatestDeployment(w http.ResponseWriter, r *http.Request) {
    d, err := svc.GetLatestDeployment(r.Context())
    if err != nil {
        httperr.Write(w, r, err)
        return
    }
    writeJSON(w, http.StatusOK, toDeploymentResponse(d))
}

Such aliases work for singleton selection — when exactly one object is returned by a given criterion. Using them as a filter for collections is not a good idea.

Logical aliases

A similar idea, but for business concepts: "default payment method", "primary address", "active plan". The user knows they have a "main" object but doesn't know its ID:

r.Get("/payment-methods/default", getDefaultPaymentMethod)
r.Get("/addresses/primary", getPrimaryAddress)
r.Get("/plans/active", getActivePlan)
func getDefaultPaymentMethod(w http.ResponseWriter, r *http.Request) {
    customerID := mustUserID(r.Context())
    pm, err := svc.GetDefaultPaymentMethod(r.Context(), customerID)
    if err != nil {
        httperr.Write(w, r, err)
        return
    }
    writeJSON(w, http.StatusOK, toPaymentMethodResponse(pm))
}

This is convenient both for the client (no need to first request the list and search for the "default" one) and for the API (the URL reads like a sentence in plain language).

Action endpoints — domain commands

Not every operation is expressed through CRUD. "Confirm the order", "cancel", "ship", "block the user" — these are domain commands that change state and often trigger side effects (events, notifications, state machine transitions).

Trying to pass such a command through PATCH /orders/{id} with a status: CANCELLED field is a poor idea. The body loses the semantics of the command, the server has a harder time validating transitions, and the documentation makes it unclear which values are allowed.

Instead, you use dedicated action endpoints: POST /{resource}/{id}/{action}.

Registering in chi

r.Route("/api/v1/orders", func(r chi.Router) {
    r.Get("/", listOrders)
    r.Post("/", createOrder)
    r.Route("/{id}", func(r chi.Router) {
        r.Get("/", getOrder)
        r.Patch("/", patchOrder)
        r.Post("/confirm", confirmOrder)
        r.Post("/cancel", cancelOrder)
        r.Post("/ship", shipOrder)
        r.Post("/refund", refundOrder)
    })
})

r.Route("/api/v1/customers", func(r chi.Router) {
    r.Route("/{id}", func(r chi.Router) {
        r.Post("/verify", verifyCustomer)
        r.Post("/block", blockCustomer)
    })
})

The action in the URL is always a verb in the infinitive (confirm, ship, refund). Not a noun (confirmation, shipment) and not the past tense (confirmed, shipped).

The method is always POST — even if the operation is idempotent. This is a convention that makes life easier for clients and intermediate layers: POST on an action URL always means "execute the command".

Handler with parameters

If a command needs additional data, it is passed in the request body:

type ShipOrderRequest struct {
    TrackingNumber string `json:"trackingNumber" validate:"required"`
    Carrier        string `json:"carrier"        validate:"required"`
}

func shipOrder(w http.ResponseWriter, r *http.Request) {
    orderID := chi.URLParam(r, "id")

    var req ShipOrderRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        httperr.Write(w, r, apperr.NewValidation("invalid request body"))
        return
    }
    if err := validate.Struct(req); err != nil {
        writeValidationProblem(w, toViolations(err.(validator.ValidationErrors)),
            traceIDFromCtx(r.Context()))
        return
    }

    order, err := svc.ShipOrder(r.Context(), orderID, req.TrackingNumber, req.Carrier)
    if err != nil {
        httperr.Write(w, r, err)
        return
    }
    writeJSON(w, http.StatusOK, toOrderResponse(order))
}

If there are no parameters, the body is empty and there's nothing to decode:

func confirmOrder(w http.ResponseWriter, r *http.Request) {
    orderID := chi.URLParam(r, "id")
    order, err := svc.ConfirmOrder(r.Context(), orderID)
    if err != nil {
        httperr.Write(w, r, err)
        return
    }
    writeJSON(w, http.StatusOK, toOrderResponse(order))
}

Action or PATCH — how to choose

A simple guideline:

  • You change a field with no business rules (description, note) → PATCH.
  • There's a domain name for the operation (confirm, ship, refund) → action.
  • The operation changes state along a state machine → action.
  • There are side effects (events, notifications, external calls) → action.

In short

  • me is an alias for the current user only in endpoints with two variants: /{id} for the administrator and /me for yourself.
  • Singleton endpoints (/profile, /settings) work with the caller from the token — me is not needed.
  • The test question: "Can an administrator access it by someone else's ID?" No → me is redundant.
  • Never make /api/v1/me without a resource prefix, only /api/v1/users/me.
  • Temporal aliases (latest, current, next) are for singleton selection of "the latest" without filtering a collection.
  • Logical aliases (default, primary, active) are a business shortcut to a single object by an attribute.
  • Action endpoints: POST /{resource}/{id}/{verb}, the verb is always in the infinitive.
  • The method for an action is always POST, even if the operation is idempotent.
  • A command with a state transition or side effects → action; a simple field with no rules → PATCH.
  • URL and resources — the base structure of routes in chi.
  • JSON and response format — how to shape a 200 OK response for an action.
  • RFC 9457 errors — how to return an error on a state conflict (409).