@@ -15,7 +15,6 @@ import (
1515// and structured error responses. The request body is capped at 1 MB.
1616//
1717// Req is decoded from the JSON request body and validated via struct tags.
18- // Res must implement Data (add an IsData() method to your response type).
1918//
2019// Error mapping:
2120// - *ValidationFailure → 422 with {"error":"validation_failed","fields":[...]}
@@ -29,7 +28,7 @@ import (
2928// return svc.CheckEmail(req.Email), nil
3029// },
3130// ))
32- func Handle [Req any , Res Data ](
31+ func Handle [Req any , Res any ](
3332 fn func (ctx context.Context , req Req ) (Res , error ),
3433) http.HandlerFunc {
3534 return func (w http.ResponseWriter , r * http.Request ) {
@@ -78,31 +77,92 @@ func HandleBatch[Req any, Item any](
7877 r .Body = http .MaxBytesReader (w , r .Body , 1 << 20 )
7978
8079 var req Req
80+
8181 if err := BindAndValidate (r , & req ); err != nil {
8282 if vf , ok := errors.AsType [* ValidationFailure ](err ); ok {
8383 ValidationError (w , vf )
8484 return
8585 }
86+
8687 Error (w , http .StatusBadRequest , "bad_request" , cleanDecodeError (err ))
8788 return
8889 }
8990
9091 res , err := fn (r .Context (), req )
92+
9193 if err != nil {
9294 if se , ok := errors.AsType [* svcerr.Error ](err ); ok {
9395 if se .Kind == svcerr .KindUpstream {
9496 sentry .CaptureException (err )
9597 }
98+
9699 Error (w , svcerr .HTTPStatus (se ), se .Code , se .Message )
97100 return
98101 }
102+
99103 sentry .CaptureException (err )
104+
100105 Error (w , http .StatusInternalServerError , "internal_error" , "internal server error" )
101106 return
102107 }
103108
104109 res .Total = len (res .Results )
110+
105111 w .Header ().Set ("X-Usage-Count" , strconv .Itoa (len (res .Results )))
112+
113+ JSON (w , http .StatusOK , res )
114+ }
115+ }
116+
117+ // HandleGet wraps a GET endpoint with automatic query-parameter binding,
118+ // validation, and structured error responses.
119+ //
120+ // Req is populated from URL query parameters using `query:"name"` struct tags
121+ // and validated via `validate:` tags. To apply defaults before binding, pass an
122+ // initialised Req value as the optional second argument — fields with no
123+ // matching query param keep their value from that initialiser.
124+ //
125+ // Error mapping is identical to Handle:
126+ // - *ValidationFailure → 422 with {"error":"validation_failed","fields":[...]}
127+ // - *svcerr.Error → mapped HTTP status with {"error":Code,"message":Message}
128+ // - any other error → 500 with {"error":"internal_error"}
129+ func HandleGet [Req any , Res any ](
130+ fn func (ctx context.Context , req Req ) (Res , error ),
131+ defaults ... Req ,
132+ ) http.HandlerFunc {
133+ return func (w http.ResponseWriter , r * http.Request ) {
134+ var req Req
135+ if len (defaults ) > 0 {
136+ req = defaults [0 ]
137+ }
138+
139+ if err := BindQuery (r , & req ); err != nil {
140+ if vf , ok := errors.AsType [* ValidationFailure ](err ); ok {
141+ ValidationError (w , vf )
142+ return
143+ }
144+
145+ Error (w , http .StatusBadRequest , "bad_request" , err .Error ())
146+ return
147+ }
148+
149+ res , err := fn (r .Context (), req )
150+
151+ if err != nil {
152+ if se , ok := errors.AsType [* svcerr.Error ](err ); ok {
153+ if se .Kind == svcerr .KindUpstream {
154+ sentry .CaptureException (err )
155+ }
156+
157+ Error (w , svcerr .HTTPStatus (se ), se .Code , se .Message )
158+ return
159+ }
160+
161+ sentry .CaptureException (err )
162+ Error (w , http .StatusInternalServerError , "internal_error" , "internal server error" )
163+ return
164+ }
165+
106166 JSON (w , http .StatusOK , res )
107167 }
108168}
@@ -121,6 +181,7 @@ func Guard[S any](svc *S, h http.HandlerFunc) http.HandlerFunc {
121181 Error (w , http .StatusInternalServerError , "internal_error" , "service unavailable" )
122182 }
123183 }
184+
124185 return h
125186}
126187
0 commit comments