Place Order Endpoint
Now let’s implement the POST endpoint that orchestrates multiple services.
Request and Response Types
Create endpoint/place_order.go:
package endpoint
import (
"context"
"errors"
"net/http"
"github.com/google/uuid"
"github.com/sourcegraph/conc/pool"
"github.com/z5labs/humus/example/rest/orders-walkthrough/service"
"github.com/z5labs/humus/rest"
)
// PlaceOrderRequest is the request body for placing an order.
type PlaceOrderRequest struct {
CustomerID string `json:"customer_id"`
AccountID string `json:"account_id"`
}
// PlaceOrderResponse is the response for a successfully placed order.
type PlaceOrderResponse struct {
OrderID string `json:"order_id"`
}
// ErrAccountRestricted indicates the account has restrictions preventing order placement.
var ErrAccountRestricted = errors.New("account has restrictions")
// ErrAccountIneligible indicates the account is not eligible to place orders.
var ErrAccountIneligible = errors.New("account is not eligible")
Note the error variable naming convention: ErrAccountRestricted follows Go’s ErrFoo pattern.
Endpoint Registration
// PlaceOrder creates the POST /v1/order endpoint.
func PlaceOrder(restrictionSvc RestrictionService, eligibilitySvc EligibilityService, dataSvc DataService) rest.ApiOption {
handler := &placeOrderHandler{
restrictionSvc: restrictionSvc,
eligibilitySvc: eligibilitySvc,
dataSvc: dataSvc,
}
return rest.Handle(
http.MethodPost,
rest.BasePath("/v1").Segment("order"),
rest.HandleJson(handler),
)
}
Key differences from GET:
- Uses
http.MethodPost - Uses
rest.HandleJson()which consumes request body AND returns response - No query parameters needed
- Interfaces defined locally (see
endpoint/interfaces.go)
Concurrent Validation with conc/pool
The handler runs validation checks concurrently for optimal performance:
type placeOrderHandler struct {
restrictionSvc RestrictionService
eligibilitySvc EligibilityService
dataSvc DataService
}
func (h *placeOrderHandler) Handle(ctx context.Context, req *PlaceOrderRequest) (*PlaceOrderResponse, error) {
// Run validation checks concurrently using conc/pool
p := pool.New().WithContext(ctx)
// Check restrictions concurrently
p.Go(func(ctx context.Context) error {
restrictions, err := h.restrictionSvc.CheckRestrictions(ctx, req.AccountID)
if err != nil {
return err
}
if len(restrictions) > 0 {
return ErrAccountRestricted
}
return nil
})
// Check eligibility concurrently
p.Go(func(ctx context.Context) error {
eligibility, err := h.eligibilitySvc.CheckEligibility(ctx, req.AccountID)
if err != nil {
return err
}
if !eligibility.Eligible {
return ErrAccountIneligible
}
return nil
})
// Wait for both checks to complete
if err := p.Wait(); err != nil {
return nil, err
}
// Create and store the order
orderID := uuid.New().String()
order := service.Order{
OrderID: orderID,
AccountID: req.AccountID,
CustomerID: req.CustomerID,
Status: service.OrderStatusPending,
}
if err := h.dataSvc.PutItem(ctx, order); err != nil {
return nil, err
}
return &PlaceOrderResponse{
OrderID: orderID,
}, nil
}
The handler demonstrates:
- Concurrent validation - Both checks run in parallel using
conc/pool - Performance optimization - ~50% latency reduction when both services are healthy
- Fail-fast behavior -
p.Wait()returns on first error - Panic safety -
conc/poolhandles panics gracefully - Context propagation - Cancellation flows to all goroutines
Why conc/pool?
The github.com/sourcegraph/conc/pool library provides:
- Structured concurrency - Automatic cleanup and error handling
- Context integration - Respects cancellation and deadlines
- Panic recovery - Converts panics to errors instead of crashing
- Production-tested - Used in Sourcegraph’s infrastructure
Registering the Endpoint
Update app/app.go to initialize the additional services and register the PlaceOrder endpoint:
// Initialize services
dataSvc := service.NewDataClient(cfg.Services.DataURL, httpClient)
restrictionSvc := service.NewRestrictionClient(cfg.Services.RestrictionURL, httpClient)
eligibilitySvc := service.NewEligibilityClient(cfg.Services.EligibilityURL, httpClient)
// Create API with both endpoints
api := rest.NewApi(
cfg.OpenApi.Title,
cfg.OpenApi.Version,
endpoint.ListOrders(dataSvc),
endpoint.PlaceOrder(restrictionSvc, eligibilitySvc, dataSvc),
)
Changes from the previous step:
- Initialize
restrictionSvcandeligibilitySvc(previously unused) - Add
endpoint.PlaceOrder()to the API registration - All three services are now wired to their respective endpoints
Testing the Endpoint
Test successful order placement:
curl -s -X POST http://localhost:8090/v1/order \
-H "Content-Type: application/json" \
-d '{"customer_id":"CUST-001","account_id":"ACC-001"}' | jq .
Response:
{
"order_id": "649cfc69-8323-4c60-8745-c7071506943d"
}
Test with restricted account:
curl -s -X POST http://localhost:8090/v1/order \
-H "Content-Type: application/json" \
-d '{"customer_id":"CUST-001","account_id":"ACC-FRAUD"}' | jq .
This will return an error because ACC-FRAUD has fraud restrictions in the Wiremock stub.
OpenAPI Schema
Check the auto-generated OpenAPI schema:
curl -s http://localhost:8090/openapi.json | jq '.paths["/v1/order"].post'
The framework automatically generates:
- Request body schema from
PlaceOrderRequeststruct - Response schema from
PlaceOrderResponsestruct - Proper content types
What’s Next
With both endpoints complete, let’s test them using Wiremock to mock the backend services.