- Compile routes at startup: `Dispatch = cowboy_router:compile([{'_', [{<<"/users/:id">>, user_handler, #{}}]}])` — always use `compile/1` for efficient matching.
- Implement `cowboy_handler` with `init(Req0, State) -> {ok, Req, State}` — pattern-match on `cowboy_req:method(Req)` to dispatch GET/POST/etc.
- Reply with `Req2 = cowboy_req:reply(200, #{<<"content-type">> => <<"application/json">>}, Body, Req)` — always return the updated `Req2`.
- Upgrade to WebSocket from `init/2`: `{cowboy_websocket, Req, State, #{idle_timeout => 60000}}` — handle frames in `websocket_handle(Frame, State)`.
- Read the full request body: `{ok, Body, Req2} = cowboy_req:read_body(Req)` — loop on `{more, Data, Req2}` to stream large bodies.
- Extract route bindings and query params: `cowboy_req:binding(id, Req)` and `cowboy_req:match_qs([{page, int, 1}, {limit, int, 20}], Req)`.
- Set headers before replying: `Req1 = cowboy_req:set_resp_header(<<"x-request-id">>, RequestId, Req)` — chain header calls before `cowboy_req:reply`.
- Use `cowboy_req:read_urlencoded_body/2` for `application/x-www-form-urlencoded` POST bodies.