Skip to main content

API Architecture

The Ordering API uses:
  • Carter: Minimal API endpoint organization
  • MediatR: Command/query dispatch
  • Mapster: Object mapping
  • FluentValidation: Automatic request validation
  • Problem Details: Standardized error responses

Base URL

http://localhost:6004
Or via API Gateway:
http://localhost:6000/ordering-service

Authentication

The API currently does not require authentication. In production, integrate with identity service.

Endpoints Overview

MethodEndpointDescription
POST/ordersCreate a new order
PUT/ordersUpdate an existing order
DELETE/orders/{id}Delete an order
GET/ordersGet all orders (paginated)
GET/orders/customer/{customerId}Get orders by customer ID
GET/orders/{orderName}Search orders by name

Create Order

Creates a new order.

Endpoint

POST /orders

Request Body

{
  "order": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "customerId": "58c49479-ec65-4de2-86e7-033c546291aa",
    "orderName": "ORD_12345",
    "shippingAddress": {
      "firstName": "John",
      "lastName": "Doe",
      "emailAddress": "[email protected]",
      "addressLine": "123 Main Street",
      "country": "USA",
      "state": "CA",
      "zipCode": "90210"
    },
    "billingAddress": {
      "firstName": "John",
      "lastName": "Doe",
      "emailAddress": "[email protected]",
      "addressLine": "123 Main Street",
      "country": "USA",
      "state": "CA",
      "zipCode": "90210"
    },
    "payment": {
      "cardName": "John Doe",
      "cardNumber": "1234567890123456",
      "expiration": "12/25",
      "cvv": "123",
      "paymentMethod": 1
    },
    "status": 2,
    "orderItems": [
      {
        "orderId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "productId": "5334c996-8457-4cf0-815c-ed2b77c4ff61",
        "quantity": 2,
        "price": 500.00
      },
      {
        "orderId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "productId": "c67d6323-e8b1-4bdf-9a75-b0d0d2e7e914",
        "quantity": 1,
        "price": 400.00
      }
    ]
  }
}

Response

Status: 201 Created Location Header: /orders/{id}
{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}

Implementation

See Ordering.API/Endpoints/CreateOrder.cs:13:
using Ordering.Application.Orders.Commands.CreateOrder;

namespace Ordering.API.Endpoints;

public record CreateOrderRequest(OrderDto Order);
public record CreateOrderResponse(Guid Id);

public class CreateOrder : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapPost("/orders", async (CreateOrderRequest request, ISender sender) =>
        {
            var command = request.Adapt<CreateOrderCommand>();
            var result = await sender.Send(command);
            var response = result.Adapt<CreateOrderResponse>();

            return Results.Created($"/orders/{response.Id}", response);
        })
        .WithName("CreateOrder")
        .Produces<CreateOrderResponse>(StatusCodes.Status201Created)
        .ProducesProblem(StatusCodes.Status400BadRequest)
        .WithSummary("Create Order")
        .WithDescription("Create Order");
    }
}

Validation Rules

  • orderName: Required, not empty
  • customerId: Required, not null
  • orderItems: Required, at least one item
  • Address fields validated by domain value objects
  • Payment CVV must be 3 characters or less

Error Responses

400 Bad Request - Validation failure:
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Order.OrderName": ["Name is required"],
    "Order.OrderItems": ["OrderItems should not be empty"]
  }
}

Update Order

Updates an existing order.

Endpoint

PUT /orders

Request Body

{
  "order": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "customerId": "58c49479-ec65-4de2-86e7-033c546291aa",
    "orderName": "ORD_12345_UPDATED",
    "shippingAddress": {
      "firstName": "Jane",
      "lastName": "Doe",
      "emailAddress": "[email protected]",
      "addressLine": "456 Oak Avenue",
      "country": "USA",
      "state": "NY",
      "zipCode": "10001"
    },
    "billingAddress": {
      "firstName": "Jane",
      "lastName": "Doe",
      "emailAddress": "[email protected]",
      "addressLine": "456 Oak Avenue",
      "country": "USA",
      "state": "NY",
      "zipCode": "10001"
    },
    "payment": {
      "cardName": "Jane Doe",
      "cardNumber": "9876543210987654",
      "expiration": "06/26",
      "cvv": "456",
      "paymentMethod": 1
    },
    "status": 3,
    "orderItems": []
  }
}

Response

Status: 200 OK
{
  "isSuccess": true
}

Implementation

See Ordering.API/Endpoints/UpdateOrder.cs:13:
using Ordering.Application.Orders.Commands.UpdateOrder;

namespace Ordering.API.Endpoints;

public record UpdateOrderRequest(OrderDto Order);
public record UpdateOrderResponse(bool IsSuccess);

public class UpdateOrder : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapPut("/orders", async (UpdateOrderRequest request, ISender sender) =>
        {
            var command = request.Adapt<UpdateOrderCommand>();
            var result = await sender.Send(command);
            var response = result.Adapt<UpdateOrderResponse>();

            return Results.Ok(response);
        })
        .WithName("UpdateOrder")
        .Produces<UpdateOrderResponse>(StatusCodes.Status200OK)
        .ProducesProblem(StatusCodes.Status400BadRequest)
        .WithSummary("Update Order")
        .WithDescription("Update Order");
    }
}

Validation Rules

  • id: Required, not empty
  • orderName: Required, not empty
  • customerId: Required, not null

Error Responses

404 Not Found - Order does not exist:
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
  "title": "Order was not found.",
  "status": 404,
  "detail": "Order with id '3fa85f64-5717-4562-b3fc-2c963f66afa6' was not found."
}

Delete Order

Deletes an order by ID.

Endpoint

DELETE /orders/{id}

Path Parameters

ParameterTypeDescription
idGuidOrder identifier

Response

Status: 200 OK
{
  "isSuccess": true
}

Implementation

See Ordering.API/Endpoints/DeleteOrder.cs:13:
using Ordering.Application.Orders.Commands.DeleteOrder;

namespace Ordering.API.Endpoints;

public record DeleteOrderResponse(bool IsSuccess);

public class DeleteOrder : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapDelete("/orders/{id}", async (Guid Id, ISender sender) =>
        {
            var result = await sender.Send(new DeleteOrderCommand(Id));
            var response = result.Adapt<DeleteOrderResponse>();

            return Results.Ok(response);
        })
        .WithName("DeleteOrder")
        .Produces<DeleteOrderResponse>(StatusCodes.Status200OK)
        .ProducesProblem(StatusCodes.Status400BadRequest)
        .ProducesProblem(StatusCodes.Status404NotFound)
        .WithSummary("Delete Order")
        .WithDescription("Delete Order");
    }
}

Validation Rules

  • id: Required, not empty

Error Responses

404 Not Found - Order does not exist:
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
  "title": "Order was not found.",
  "status": 404,
  "detail": "Order with id '3fa85f64-5717-4562-b3fc-2c963f66afa6' was not found."
}

Get Orders (Paginated)

Retrieves all orders with pagination.

Endpoint

GET /orders?pageIndex=0&pageSize=10

Query Parameters

ParameterTypeRequiredDefaultDescription
pageIndexintNo0Zero-based page index
pageSizeintNo10Number of items per page

Response

Status: 200 OK
{
  "orders": {
    "pageIndex": 0,
    "pageSize": 10,
    "count": 42,
    "data": [
      {
        "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "customerId": "58c49479-ec65-4de2-86e7-033c546291aa",
        "orderName": "ORD_12345",
        "shippingAddress": {
          "firstName": "John",
          "lastName": "Doe",
          "emailAddress": "[email protected]",
          "addressLine": "123 Main Street",
          "country": "USA",
          "state": "CA",
          "zipCode": "90210"
        },
        "billingAddress": {
          "firstName": "John",
          "lastName": "Doe",
          "emailAddress": "[email protected]",
          "addressLine": "123 Main Street",
          "country": "USA",
          "state": "CA",
          "zipCode": "90210"
        },
        "payment": {
          "cardName": "John Doe",
          "cardNumber": "1234567890123456",
          "expiration": "12/25",
          "cvv": "123",
          "paymentMethod": 1
        },
        "status": 2,
        "orderItems": [
          {
            "orderId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
            "productId": "5334c996-8457-4cf0-815c-ed2b77c4ff61",
            "quantity": 2,
            "price": 500.00
          }
        ]
      }
    ]
  }
}

Implementation

See Ordering.API/Endpoints/GetOrders.cs:13:
using BuildingBlocks.Pagination;
using Ordering.Application.Orders.Queries.GetOrders;

namespace Ordering.API.Endpoints;

public record GetOrdersResponse(PaginatedResult<OrderDto> Orders);

public class GetOrders : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapGet("/orders", async ([AsParameters] PaginationRequest request, ISender sender) =>
        {
            var result = await sender.Send(new GetOrdersQuery(request));
            var response = result.Adapt<GetOrdersResponse>();

            return Results.Ok(response);
        })
        .WithName("GetOrders")
        .Produces<GetOrdersResponse>(StatusCodes.Status200OK)
        .ProducesProblem(StatusCodes.Status400BadRequest)
        .ProducesProblem(StatusCodes.Status404NotFound)
        .WithSummary("Get Orders")
        .WithDescription("Get Orders");
    }
}

Response Fields

PaginatedResult:
  • pageIndex: Current page (zero-based)
  • pageSize: Items per page
  • count: Total number of orders
  • data: Array of order DTOs
OrderStatus Enum:
  • 1: Draft
  • 2: Pending
  • 3: Completed
  • 4: Cancelled

Get Orders By Customer

Retrieves all orders for a specific customer.

Endpoint

GET /orders/customer/{customerId}

Path Parameters

ParameterTypeDescription
customerIdGuidCustomer identifier

Response

Status: 200 OK
{
  "orders": [
    {
      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "customerId": "58c49479-ec65-4de2-86e7-033c546291aa",
      "orderName": "ORD_12345",
      "shippingAddress": { /* ... */ },
      "billingAddress": { /* ... */ },
      "payment": { /* ... */ },
      "status": 2,
      "orderItems": [ /* ... */ ]
    }
  ]
}

Implementation

See Ordering.API/Endpoints/GetOrdersByCustomer.cs:12:
using Ordering.Application.Orders.Queries.GetOrdersByCustomer;

namespace Ordering.API.Endpoints;

public record GetOrdersByCustomerResponse(IEnumerable<OrderDto> Orders);

public class GetOrdersByCustomer : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapGet("/orders/customer/{customerId}", async (Guid customerId, ISender sender) =>
        {
            var result = await sender.Send(new GetOrdersByCustomerQuery(customerId));
            var response = result.Adapt<GetOrdersByCustomerResponse>();

            return Results.Ok(response);
        })
        .WithName("GetOrdersByCustomer")
        .Produces<GetOrdersByCustomerResponse>(StatusCodes.Status200OK)
        .ProducesProblem(StatusCodes.Status400BadRequest)
        .ProducesProblem(StatusCodes.Status404NotFound)
        .WithSummary("Get Orders By Customer")
        .WithDescription("Get Orders By Customer");
    }
}

Notes

  • Returns empty array if customer has no orders
  • Includes all order items
  • Sorted by order name

Get Orders By Name

Searches orders by name (partial match).

Endpoint

GET /orders/{orderName}

Path Parameters

ParameterTypeDescription
orderNamestringOrder name search term (partial match)

Response

Status: 200 OK
{
  "orders": [
    {
      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "customerId": "58c49479-ec65-4de2-86e7-033c546291aa",
      "orderName": "ORD_12345",
      "shippingAddress": { /* ... */ },
      "billingAddress": { /* ... */ },
      "payment": { /* ... */ },
      "status": 2,
      "orderItems": [ /* ... */ ]
    }
  ]
}

Implementation

See Ordering.API/Endpoints/GetOrdersByName.cs:12:
using Ordering.Application.Orders.Queries.GetOrdersByName;

namespace Ordering.API.Endpoints;

public record GetOrdersByNameResponse(IEnumerable<OrderDto> Orders);

public class GetOrdersByName : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapGet("/orders/{orderName}", async (string orderName, ISender sender) =>
        {
            var result = await sender.Send(new GetOrdersByNameQuery(orderName));
            var response = result.Adapt<GetOrdersByNameResponse>();

            return Results.Ok(response);
        })
        .WithName("GetOrdersByName")
        .Produces<GetOrdersByNameResponse>(StatusCodes.Status200OK)
        .ProducesProblem(StatusCodes.Status400BadRequest)
        .ProducesProblem(StatusCodes.Status404NotFound)
        .WithSummary("Get Orders By Name")
        .WithDescription("Get Orders By Name");
    }
}

Notes

  • Performs case-sensitive partial match (contains)
  • Returns empty array if no matches found
  • Includes all order items
  • Sorted by order name

Data Models

OrderDto

interface OrderDto {
  id: string;              // UUID
  customerId: string;      // UUID
  orderName: string;
  shippingAddress: AddressDto;
  billingAddress: AddressDto;
  payment: PaymentDto;
  status: OrderStatus;     // 1=Draft, 2=Pending, 3=Completed, 4=Cancelled
  orderItems: OrderItemDto[];
}

AddressDto

interface AddressDto {
  firstName: string;
  lastName: string;
  emailAddress: string;
  addressLine: string;
  country: string;
  state: string;
  zipCode: string;
}

PaymentDto

interface PaymentDto {
  cardName: string;
  cardNumber: string;
  expiration: string;      // Format: MM/YY
  cvv: string;             // 3 digits
  paymentMethod: number;   // Payment method type
}

OrderItemDto

interface OrderItemDto {
  orderId: string;         // UUID
  productId: string;       // UUID
  quantity: number;        // Must be > 0
  price: number;           // Must be > 0
}

PaginatedResult

interface PaginatedResult<T> {
  pageIndex: number;       // Zero-based page index
  pageSize: number;        // Items per page
  count: number;           // Total items
  data: T[];              // Page data
}

Error Handling

All endpoints return standardized Problem Details responses for errors.

Validation Error (400)

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "fieldName": ["Error message"]
  }
}

Not Found Error (404)

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
  "title": "Resource was not found.",
  "status": 404,
  "detail": "Detailed error message"
}

Internal Server Error (500)

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.6.1",
  "title": "An error occurred while processing your request.",
  "status": 500
}

Rate Limiting

Currently not implemented. Consider adding rate limiting for production.

Testing with cURL

Create Order

curl -X POST http://localhost:6004/orders \
  -H "Content-Type: application/json" \
  -d '{
    "order": {
      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "customerId": "58c49479-ec65-4de2-86e7-033c546291aa",
      "orderName": "ORD_12345",
      "shippingAddress": {
        "firstName": "John",
        "lastName": "Doe",
        "emailAddress": "[email protected]",
        "addressLine": "123 Main St",
        "country": "USA",
        "state": "CA",
        "zipCode": "90210"
      },
      "billingAddress": {
        "firstName": "John",
        "lastName": "Doe",
        "emailAddress": "[email protected]",
        "addressLine": "123 Main St",
        "country": "USA",
        "state": "CA",
        "zipCode": "90210"
      },
      "payment": {
        "cardName": "John Doe",
        "cardNumber": "1234567890123456",
        "expiration": "12/25",
        "cvv": "123",
        "paymentMethod": 1
      },
      "status": 2,
      "orderItems": [
        {
          "orderId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
          "productId": "5334c996-8457-4cf0-815c-ed2b77c4ff61",
          "quantity": 2,
          "price": 500.00
        }
      ]
    }
  }'

Get Orders

curl http://localhost:6004/orders?pageIndex=0&pageSize=10

Get Orders By Customer

curl http://localhost:6004/orders/customer/58c49479-ec65-4de2-86e7-033c546291aa

Delete Order

curl -X DELETE http://localhost:6004/orders/3fa85f64-5717-4562-b3fc-2c963f66afa6

Integration Events

Consumed Events

BasketCheckoutEvent - Published by Basket service Triggered when customer completes checkout. Automatically creates an order. Event Structure:
public record BasketCheckoutEvent
{
    public Guid CustomerId { get; init; }
    public string UserName { get; init; }
    public decimal TotalPrice { get; init; }
    
    // Shipping Address
    public string FirstName { get; init; }
    public string LastName { get; init; }
    public string EmailAddress { get; init; }
    public string AddressLine { get; init; }
    public string Country { get; init; }
    public string State { get; init; }
    public string ZipCode { get; init; }
    
    // Payment
    public string CardName { get; init; }
    public string CardNumber { get; init; }
    public string Expiration { get; init; }
    public string CVV { get; init; }
    public int PaymentMethod { get; init; }
}

Published Events

OrderDto (Order Created) - Published to message broker Published when new order is created (feature flag controlled). Feature Flag: OrderFullfilment Event Structure: Same as OrderDto model above

Performance Considerations

Query Optimization

  • All queries use AsNoTracking() for read operations
  • Related entities eager loaded with Include()
  • Pagination prevents large result sets

Caching Strategy

Consider adding caching for:
  • Customer order lists
  • Order details (short TTL)
  • Product reference data

Database Indexing

Recommended indexes:
CREATE INDEX IX_Orders_CustomerId ON Orders(CustomerId);
CREATE INDEX IX_Orders_OrderName ON Orders(OrderName);
CREATE INDEX IX_Orders_Status ON Orders(Status);
CREATE INDEX IX_OrderItems_OrderId ON OrderItems(OrderId);
CREATE INDEX IX_OrderItems_ProductId ON OrderItems(ProductId);

Next Steps

Build docs developers (and LLMs) love