This Shopping Cart example demonstrates Mixon's advanced REST API capabilities including:
- HATEOAS (Hypermedia as the Engine of Application State)
- Content Negotiation with multiple media types (JSON, HAL+JSON, HTML)
- HTMX Integration for dynamic client-side interactions
- Workflow State Management with a full state machine implementation
Unlike the Product Catalog example (which focuses on basic CRUD operations), this example showcases how to build a hypermedia-driven API that follows REST architectural constraints more completely.
You can run this example using Deno tasks:
# Run the workflow example
deno task workflow
# Run with file watching (auto-reload on changes)
deno task workflow:watch
Then open your browser to http://localhost:3000 to see the application.
- Hypermedia Controls: API responses include links to related resources and available actions
- Resource Discovery: Clients can navigate the API without prior knowledge of endpoints
- State Transitions: Available actions are presented based on the current state
- Self-Documenting API: Responses include metadata about what actions are possible
- Multiple Media Types: Supports JSON, HAL+JSON, and HTML from the same endpoints
- Accept Header Processing: Automatically selects the appropriate representation based on the client's Accept header
- HAL Format: Hypermedia Application Language format for machine clients
- HTML Format: Rich HTML responses for browser clients
- HTMX Integration: Dynamic content swapping without writing JavaScript
- Nano JSX: Server-side rendering with JSX components
- Interactive UI: Search, sorting, and dynamic updates
- Single-Page Application: Navigation without page reloads
- Responsive Design: Works on mobile and desktop
- Home: Overview of the application
- Products: Interactive product catalog with search and sorting
- Features: Showcase of HTMX capabilities
- API Formats: Examples of different response formats
- Error Demo: Demonstration of error handling
- Content swapping with
hx-get
andhx-target
- Form submission with
hx-post
- Search with debounce using
hx-trigger="keyup changed delay:500ms"
- Loading indicators with
hx-indicator
- Lazy loading with
hx-trigger="load"
- Polling with
hx-trigger="load, every 2s"
The UI is built with reusable components:
Layout
: Main page layout with navigation and content areaHome
: Home page contentProductList
: Product catalog with search and sortingProductCard
: Individual product card componentProductDetail
: Detailed product viewFeatures
: HTMX features showcaseFeatureCard
: Individual feature card componentApiFormats
: API format examplesErrorDemo
: Error handling demonstration
docWorkflow.load({
transitions: [
{ from: "Draft", to: "Review", on: "Submit", ... }
]
});
doc.history.push({
from: "Draft",
to: "Review",
at: new Date(),
by: "user@company.com",
comments: "Initial submission"
});
const task = docWorkflow.getTaskForTransition(event, doc.state);
sendEmail(task.assign, task.message);
if (!docWorkflow.canTransition(event, doc.state)) {
return ctx.respond({ error: "Invalid transition" });
}
// Using the createLinks utility to generate hypermedia controls
const links = createLinks({
self: `/documents/${doc.id}`,
history: `/documents/${doc.id}/history`,
revert: `/documents/${doc.id}/revert`,
// Only include transition links for valid state transitions
...(canApprove ? { approve: `/documents/${doc.id}/approve` } : {}),
...(canReject ? { reject: `/documents/${doc.id}/reject` } : {})
});
// Include links in the response
ctx.response = createResponse(ctx, document, { links });
{
"id": "doc-123",
"title": "Project Proposal",
"state": "Review",
"content": "...",
"_links": {
"self": { "href": "/documents/doc-123" },
"history": { "href": "/documents/doc-123/history" },
"approve": { "href": "/documents/doc-123/approve" },
"reject": { "href": "/documents/doc-123/reject" }
}
}
// Parse Accept header for content negotiation
const acceptHeader = request.headers.get('Accept');
const preferredMediaType = parseAcceptHeader(acceptHeader);
// Return response in the preferred format
return match({ mediaType: preferredMediaType })
.with({ mediaType: MediaType.HAL }, () => {
// Return HAL+JSON format with _links
return new Response(JSON.stringify({
...document,
_links: links
}), {
headers: { "Content-Type": MediaType.HAL }
});
})
.with({ mediaType: MediaType.JSON }, () => {
// Return plain JSON format
return new Response(JSON.stringify(document), {
headers: { "Content-Type": MediaType.JSON }
});
})
.with({ mediaType: MediaType.HTML }, () => {
// Return HTML format using JSX components
return new Response(renderSSR(
<DocumentDetail document={document} links={links} />
), {
headers: { "Content-Type": MediaType.HTML }
});
})
.otherwise(() => {
// Default to JSON if no match
return new Response(JSON.stringify(document), {
headers: { "Content-Type": MediaType.JSON }
});
});
curl http://localhost:3000/api/products \
-H "Accept: application/json"
Response:
{
"products": [
{ "id": "p001", "name": "Wireless Mouse", "price": 29.99, ... },
{ "id": "p002", "name": "Mechanical Keyboard", "price": 89.99, ... }
]
}
curl http://localhost:3000/api/products \
-H "Accept: application/hal+json"
Response:
{
"products": [
{ "id": "p001", "name": "Wireless Mouse", "price": 29.99, ... },
{ "id": "p002", "name": "Mechanical Keyboard", "price": 89.99, ... }
],
"_links": {
"self": { "href": "/api/products" },
"search": { "href": "/api/products/search" },
"categories": { "href": "/api/products/categories" }
}
}
curl http://localhost:3000/api/products \
-H "Accept: text/html"
Response: HTML content for browser rendering
curl http://localhost:3000/api/products/p001 \
-H "Accept: application/hal+json"
Response:
{
"id": "p001",
"name": "Wireless Mouse",
"price": 29.99,
"description": "Ergonomic wireless mouse with long battery life",
"category": "electronics",
"_links": {
"self": { "href": "/api/products/p001" },
"add-to-cart": { "href": "/api/cart/add" },
"category": { "href": "/api/products?category=electronics" },
"related": { "href": "/api/products/related/p001" }
}
}
curl http://localhost:3000/api/products?category=electronics \
-H "Accept: application/hal+json"
curl -X POST http://localhost:3000/api/cart/add \
-H "Content-Type: application/json" \
-d '{
"productId": "p001",
"quantity": 1
}'
curl http://localhost:3000/api/orders/o123 \
-H "Accept: application/hal+json"
Response:
{
"id": "o123",
"status": "pending",
"items": [...],
"total": 119.98,
"_links": {
"self": { "href": "/api/orders/o123" },
"cancel": { "href": "/api/orders/o123/cancel" },
"pay": { "href": "/api/orders/o123/pay" }
}
}
curl -X POST http://localhost:3000/api/orders/o123/pay \
-H "Content-Type: application/json" \
-d '{
"paymentMethod": "credit_card",
"cardNumber": "4111111111111111",
"expiryDate": "12/25",
"cvv": "123"
}'
curl http://localhost:3000/api/orders/o123 \
-H "Accept: application/hal+json"
Response:
{
"id": "o123",
"status": "paid",
"items": [...],
"total": 119.98,
"_links": {
"self": { "href": "/api/orders/o123" },
"receipt": { "href": "/api/orders/o123/receipt" },
"shipping": { "href": "/api/orders/o123/shipping" }
}
}
Notice how the available actions (links) change based on the order state. This is a key aspect of HATEOAS - the server guides the client through the application by providing relevant links based on the current state.
stateDiagram-v2
[*] --> Draft
Draft --> Review: Submit
Review --> Approved: Approve
Review --> Rejected: Reject
Rejected --> Draft: Revise
Approved --> Archived: Archive
- Dynamic Navigation Links: API responses include links that change based on resource state
- Self-Documenting API: Clients can discover available actions from the API itself
- Decoupled Client-Server: Clients don't need to hardcode URL patterns or state transitions
- State-Driven Interactions: Available actions are determined by the current state of resources
- Multiple Representations: Same resources available in different formats (JSON, HAL+JSON, HTML)
- Client Preference: Server respects the client's preferred format via Accept headers
- Progressive Enhancement: HTML responses include HTMX attributes for enhanced browser experience
- Format-Specific Features: HAL+JSON includes hypermedia controls, HTML includes UI components
- Full State Machine Implementation: Formal definition of states, events, and transitions
- Audit Trail: Complete history of state changes with user attribution
- Transition Validation: Ensures only valid state transitions are allowed
- Type-Safe Event Handling: Strong typing for workflow events and states
- Evolvable API: Server can change implementation details without breaking clients
- Discoverable Functionality: Clients can learn about API capabilities at runtime
- Reduced Client Complexity: Clients don't need to implement complex state management
- Consistent Error Handling: Standardized error responses across all endpoints
- Improved Developer Experience: Clear separation of concerns between client and server
This example demonstrates how to build truly RESTful APIs that go beyond simple CRUD operations to embrace the full power of the REST architectural style, including the often-overlooked HATEOAS constraint.