The Shopify App store has a number of great apps that use Shopify's carrier service to provide specialized shipping rates to customers. Plus and Enterprise merchants on Shopify often times require shipping rates that are closely tied to their business logic or specialized rates that they have negotiated across a number of different carriers.
We're going to create a simple carrier service that returns calculated shipping rates based on a few different variables. This will be a two part series, first building a simple Minimal API in .NET C# that returns rates in the format Shopify is expecting; and second, wiring up the app so that it can be installed on a Shopify store as a custom app.
To get started, let's talk through our solution, and three associated projects.
dotnet new sln
dotnet new classlib --name CarrierService.Domain
dotnet sln add CarrierService.Domain/CarrierService.Domain.csproj
dotnet new web --name CarrierService.WebService
dotnet sln add CarrierService.WebService/CarrierService.WebService.csproj
dotnet new mstest --name CarrierService.Tests
dotnet sln add CarrierService.Tests/CarrierService.Tests.csproj
Domain
This project will contain the domain models and functionality of our shipping rates microservice, separating it from our Tests project.
WebService
This will contain our Minimal API that receives a shipping rate request and returns rates.
Tests
This will contain our tests, which we will evolve as we build this project.
Shipping Rates
The carrier service has the following example request and response objects:
Request
{
"rate": {
"origin": {
"country": "CA",
"postal_code": "K2P1L4",
"province": "ON",
"city": "Ottawa",
"name": null,
"address1": "150 Elgin St.",
"address2": "",
"address3": null,
"phone": null,
"fax": null,
"email": null,
"address_type": null,
"company_name": "Jamie D's Emporium"
},
"destination": {
"country": "CA",
"postal_code": "K1M1M4",
"province": "ON",
"city": "Ottawa",
"name": "Bob Norman",
"address1": "24 Sussex Dr.",
"address2": "",
"address3": null,
"phone": null,
"fax": null,
"email": null,
"address_type": null,
"company_name": null
},
"items": [
{
"name": "Short Sleeve T-Shirt",
"sku": "",
"quantity": 1,
"grams": 1000,
"price": 1999,
"vendor": "Jamie D's Emporium",
"requires_shipping": true,
"taxable": true,
"fulfillment_service": "manual",
"properties": null,
"product_id": 48447225880,
"variant_id": 258644705304
}
],
"currency": "USD",
"locale": "en"
}
}
Response
{
"rates": [
{
"service_name": "canadapost-overnight",
"service_code": "ON",
"total_price": "1295",
"description": "This is the fastest option by far",
"currency": "CAD",
"min_delivery_date": "2013-04-12 14:48:45 -0400",
"max_delivery_date": "2013-04-12 14:48:45 -0400"
},
{
"service_name": "fedex-2dayground",
"service_code": "2D",
"total_price": "2934",
"currency": "USD",
"min_delivery_date": "2013-04-12 14:48:45 -0400",
"max_delivery_date": "2013-04-12 14:48:45 -0400"
},
{
"service_name": "fedex-priorityovernight",
"service_code": "1D",
"total_price": "3587",
"currency": "USD",
"min_delivery_date": "2013-04-12 14:48:45 -0400",
"max_delivery_date": "2013-04-12 14:48:45 -0400"
}
]
}
Generating the Domain Models
GitHub Copilot can be used to reduce some of our mundane effort, and we can use the above request and response examples to help us generate the domain models that we'll use in this project. Depending on how you architect your project, you'll most likely want to use DTOs to abstract from your domain models. We will also be using Microsoft's JsonSerializer class for JSON serialization due to it's speed and simplicity.
The GitHub example project has the following DTOs:
- DTOAddress.cs: Used for address entities
- DTOLineItem.cs: Used for cart line items
- DTORate.cs: The returned rate used by Shopify
- DTORateRequest.cs: The wrapper class that contains the rate request details
- DTORateRequestDetails.cs: The class that contains the origin and destination addresses, cart line items, currency, and locale
- DTORateResponse.cs: The wrapper class that contains the list of available rates
When you review the project, you'll see that I've abstracted the DTOs from the domain models that I will use later on in this project. We're using the factory pattern to map our DTO to base models in the solution. I decided to manually map this in this instance due to using a limited number of DTOs and models. If your solution becomes more complex, you may want to consider using a tool to map your DTOs to models such as AutoMapper.
The Web Service
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", async (HttpContext context) =>
{
// Deserialize the incoming JSON request body
using var reader = new StreamReader(context.Request.Body);
var requestBody = await reader.ReadToEndAsync();
var rateRequest = new DTORateRequest();
try
{
rateRequest = JsonSerializer.Deserialize<DTORateRequest>(requestBody);
}
catch (JsonException)
{
context.Response.StatusCode = 400; // Bad Request
await context.Response.WriteAsync("Invalid JSON.");
return;
}
if (rateRequest == null)
{
context.Response.StatusCode = 400; // Bad Request
await context.Response.WriteAsync("Invalid request.");
return;
}
// Map valid request DTO to domain object
var rateRequestDetails = RateRequestFactory.MapFromDTO(rateRequest);
// Get a list of rates
var rateFactory = new RateFactory(rateRequestDetails);
var rates = rateFactory.GetMockRates();
var rateResponse = DTORateResponseFactory.BuildDTORateResponse(rates);
// Serialize the list of rates to JSON
var response = JsonSerializer.Serialize(rateResponse);
// Return the JSON response
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(response);
});
app.Run();
The Minimal API web service example is above. This works by listening for the request that Shopify sends during checkout, deserializes the request, maps the DTO to our domain models, gets mock rates, builds the DTO response and sends the rate response back.
Shopify has some special conditions as well that we can use from an HTTP response standpoint if things go wrong.
To indicate that this carrier service cannot handle this shipping request, return an empty array and any successful (20x) HTTP code.
To force backup rates instead, return a 40x or 50x HTTP code with any content. A good choice is the regular 404 Not Found code.
Redirects (30x codes) will only be followed for the same domain as the original callback URL. Attempting to redirect to a different domain will trigger backup rates.
There is no retry mechanism. The response must be successful on the first try, within the time budget listed below. Timeouts or errors will trigger backup rates.
For our current use case, we will return a 400 response code if the request can't be deserialized, or if the request is null.
Testing Our Example
Using our example solution, you can see what we've built by executing the following.
cd CarrierService.WebService
dotnet build
dotnet run
Using Postman, you can POST the above request object from Shopify's developer documentation to the endpoint that your API is setup to use. In my example, we're using http://localhost:5196. If you've setup everything successfully, you should see the example response we have documented above.
These are the mock rates that are generated by our project.
Next Steps
The example we have is very basic at the moment, and needs the following to be a full fledged Shopify carrier service.
- Returning actual rates instead of mock rates
- Additional features to wire this up as a carrier service app on a test store.
- Additional real-world performance to ensure that our responses will be significantly faster than the response timeouts
- Under 1500 RPM, 10s
- 1500 to 3000 RPM, 5s
- Over 3000, 3s
Please stay tuned for Part II, which will talk through the above.