Organize requests by response?

We’re currently in the process of creating an API.

We’re using the Open API Spec (3 something) and we have some requests in PM.

I started creating some smoke tests only to realize that the response body is different for the requests even though they use the same functionality.

For example,
Get /status_Items

"data": {

        "statusItem": [

            {

Get /trays/:tray_id/tracking_detail

{
    "data": {
        "trackingDetail": {

Both of these fall under the functionality of “Tracking” at least that’s how we currently have them grouped.

The only solution I’m seeing now is to group all requests by response structure.

Maybe by functionality and then subfolders arranged by response body?

Does this sound like a good\bad idea? Is there an even better way that I’m not seeing?

Also, I’m shuddering as to how we’d name these folders.

I am not sure what you are trying to accomplish with the collection, but arranging by the response body does not sound right.

I would rather organize requests/folder by the business functionality / workflow steps.

Just make sure you first have a bunch of requests to get an overview and play around organizing them.

The latest change I made was to organize by functionality or business need, then under that, I organize the requests into folders based on their endpoints. The name of the folder though is still related to the business need.

That way I can put the tests at folder level and use the same json response body test and schema test because it’s the same structure.

Thoughts or suggestions?

PMCollStruct

I don’t see how customers (which should return an array of customer objects) can have the same response body as customers/:id which will return a single customer object.

They both return an array of objects. Except when using :id, the array only has one object.

/customers

{
    "data": {
        "customer": [
            {
                "ID": 1,
                "addPricelistID": 0,
                "billToID": 0,
                "buyinGroupID": 0,
                "cardTypeID": 0,
//...more properties and more customers

/customers/:id

{
    "data": {
        "customer": [
            {
                "ID": 1,
                "addPricelistID": 0,
                "billToID": 0,
                "buyinGroupID": 0,
                "cardTypeID": 0,
// Only returns customer who's ID is 1.

Is this a bad practice?

It is just unexpected. Can there be two customers with the same id? What I don’t like is that I have to make assumptions about it (like getting the first element from the array).

What happens if I enter an invalid customer id? Do I get 404 or an empty list?

The customer ID is the primary key so there can be only one. Like Highlander without the swords.

If you enter an invalid customer id, you get a 404 and the following response body.

{
    "error": {
        "errorID": 31964,
        "message": "The server has not found anything matching the request"
    }
}

As a reminder, this is our first REST API.

Is this what you’d expect, or is this a bad practice?

Are there REST guidelines that you use to guide you through the creation process?

Any pointers or resources would be greatly appreciated.

I wouldn’t expect this behavior. I would expect exactly what @vdespa said.

In a RESTful API, I would expect an endpoint like /customers to return an array of customers.

However, if I was looking for a specific customer, like /customers/:id I would expect the response to be the customer object with full details about the customer. The list should return a subset of information for customers, but looking up by an id should return a single object with all details.

For reference, you could read this blog post by Swagger on proper API design.

1 Like

So I asked the dev about this and he said it was to avoid polymorphic results.

I do have to say that it makes testing easier because I can use single tests on multiple responses.

Say I want to check that the response properties exist. Maybe as part of a smoke test that checks for ID and CustomerName.

Now I can check two responses using the same test. If the response structure was different, I’d have to write two tests.

Actually 3 in this case, because all of these reponses have the same structure.
NOTE: In this example, address_detail will have an extra embedded array with the customer address\es
/customers
/customers/:id
/customers/:id/address_detail

    "data": {
        "customer": [
            {
                "ID": 75,
                "addPricelistID": 0,
                "billToID": 0,
                "buyinGroupID": 0,

This seems like a good way to get the most bang for our buck from a testing standpoint.

What issues will this cause?

So /customers/:id/address_detail returns the exact same information as the other two endpoints?

Why bother having it at all, then?

If this is an internal API, I don’t see there being any issues if everyone understands this is how the developers are building it.

If this is external/public, you aren’t adhering to standard best practices, which would cause confusion among your consumers.

It would be best practice to think in terms of resources. I am getting a customer resource or a list of customers.

/customers/:id/address_detail
doesn’t have the exact same structure.
In my last post I added this note.
NOTE: In this example, address_detail will have an extra embedded array with the customer address\es

The beginning of the response structure is the same though.

{
    "data": {
        "customer": [
            {
                "ID": 75,
                "addPricelistID": 0,
                "billToID": 0,
                "buyinGroupID": 0,
                "cardTypeID": 0,
                "chainID": 0,
                "commentID": 0,
                "creditIndicatorID": 0,
                "currencyID": 1,
                "discountByValueID": 0,
                "discountPromptPayID": 0,
                "EFTInstitutionID": 0,
                "finEdgedPricelistID": 0,
                "finUncutPricelistID": 0,
                "framePricelistID": 0,
                "freightPricelistID": 0,
                "GLAccountID": 0,
                "invoiceCycleID": 0,
                "languageID": 0,
                "miscItemPricelistID": 0,
                "OSPricelistID": 0,
                "packagePricelistID": 0,
                "payCycleID": 0,
                "payingAgentID": 0,
                "paymentTermsID": 1,
                "priceListID": 16,
                "prismPricelistID": 0,
                "rxEdgedPricelistID": 0,
                "rxUncutPricelistID": 0,
                "salesRepID": 2,
                "shippingMethodID": 0,
                "shippingServerID": 0,
                "shippingZoneID": 0,
                "shipToID": 0,
                "specialPriceListID": 0,
                "specificEOMCodeID": 0,
                "statementDiscountID": 0,
                "stockLensPricelistID": 0,
                "taxAuthorityID": 1,
                "volumeDiscountID": 0,
                "accountNumber": "11234",
                "accountsEmailAddress": "",
                "allowBackOrders": false,
                "allowPartialShipping": false,
                "balancingMethod": 0,
                "cardExpiryMonth": 0,
                "cardExpiryYear": 0,
                "cardHolderName": "",
                "cardNumber": "0",
                "cardSecurityCode": 0,
                "name": "Angela Suhail",
                "dateCreated": "2018-06-07T01:00:00.000Z",
                "defaultPaymentType": 0,
                "detailedInvoice": false,
                "discount": 0,
                "discountEndDate": "",
                "EFTAccount": "",
                "EFTReference": "",
                "EFTSortCode": "",
                "emailDueReport": false,
                "emailInvoice": false,
                "emailOverdueReport": false,
                "emailStatements": false,
                "excludeFromReports": false,
                "exportDeclaration": "",
                "faxNumber": "",
                "financeCharge": false,
                "fixedCharge": false,
                "fixedShippingCharge": 0,
                "freeShippingIn": false,
                "freeShippingOut": false,
                "freightCharge": 0,
                "freightCode": "",
                "generalEmailAddress": "",
                "groupByPackingSlip": false,
                "hidePatientNameOnWIP": false,
                "invoiceMethod": 0,
                "isActive": false,
                "isInternal": false,
                "isLab": false,
                "isPOMandatory": false,
                "lastCycleInvoiceDate": "",
                "lastCycleRun": "",
                "linkedLab": 0,
                "listID": "",
                "patientMandatory": false,
                "payByCard": false,
                "payByEFT": false,
                "phoneNumber": "",
                "POMandatory": false,
                "priceOrders": true,
                "printByOriginator": false,
                "pwd": "",
                "pwdRequired": false,
                "rxInvoicesToPrint": 1,
                "salesTaxAltCode": "",
                "salesTaxPrimaryCode": "",
                "sendNotifications": false,
                "sendWIPEmail": false,
                "sendWIPFax": false,
                "sendWIPReport": false,
                "shipToCustomer": false,
                "statementCycle": 0,
                "statementDiscount": 0,
                "stockInvoicesToPrint": 1,
                "storeCode": "",
                "superInvoicing": false,
                "suppressPricingOnDoc": false,
                "suppressPricingOnInv": false,
                "taxExempNumber": "",
                "taxRegNumber": "",
                "useInvoiceCycles": false,
                "usePayingAgent": false,
                "volumeDiscount": false,
                "WIPEmailAddress": "",
                "WIPFaxNumber": "",
                "WIPInternetAccess": false,
                "WIPReportEndTime": "",
                "WIPReportStartTime": "",
                "createdBy": 0,
                "updatedBy": 22,
                "lastUpdated": "2020-01-12T16:50:24.496Z",
                "addresses": [ **//This is the extra array**
                    {
                        "ID": 75,
                        "countryID": 0,
                        "countyID": 0,
                        "customerID": 75,
                        "regionID": 0,
                        "addressType": 7,
                        "externalID": "",
                        "line1": "",
                        "line2": "",
                        "line3": "",
                        "locality": "",
                        "postcode": "",
                        "createdBy": 0,
                        "updatedBy": 0,
                        "lastUpdated": "2018-06-07T17:06:51.135Z"
                    }
                ]
            }
        ]
    }
}

So when I’m testing, I can still test multiple requests with this test.

const jsonData = pm.response.json();
pm.test("Customers smoke test", () => {
pm.expect(jsonData.data.customer).to.be.an("array");
pm.expect(jsonData.data.customer[0]).to.be.an("object");
pm.expect(jsonData.data.customer[0]).to.have.property("ID");
pm.expect(jsonData.data.customer[0]).to.have.property("billToID");
pm.expect(jsonData.data.customer[0]).to.have.property("name");   
 });

To test the customer address array, I guess I’d have to add a conditional to that test so IF there’s a an array called “addresses” it checks for properties in that array.

                "addresses": [ **//This is the extra array**
                    {
                        "ID": 75,
                        "countryID": 0,
                        "countyID": 0,
//...

That way I could smoke tests 3 requests with one code block.

I’m not sure if doing that is good\bad or unimportant. It is a basic smoke test.

"It would be best practice to think in terms of resources. I am getting a customer resource or a list of customers."

Will doing it our way cause problems though? It doesn’t seem like a big deal to have an array with a single item for requests that use a unique identifier. But that’s just my inexperienced mind talking.

Where could this trip us up in the future?

BTW I really appreciate all of your feedback on this. We’re truly new to this and I’m learning so much.

There will be extra-complexity in coding & testing. It is a good idea to look at some best practices in terms of design.

Why have a data object that contains another array that contains an object?

The powers that be decided on starting all responses with a “data” object. I didn’t get much explanation beyond that.

Now as far as " Why have a data object that contains another array that contains an object".

I guess you’re talking about the addresses array?

That’s an array because multiple addresses are possible. BillTo and ShipTo for example.

/customers
returns all customers, but not their address information, just the address id’s “billToID” and “shipToID”

/customers/:id
returns a customer, but no address information like above

/customers/:id/address_detail
returns a customer with the details of their address.

Would you suggest that we have a single endpoint that returns all customers with address detail information?

For example, if there’s no :id provided it gives us everything, if an :id is provided it gives us a single customer; no matter what, we get all the address information?

Look at the API design as a learn process. You can’t get everything right from the start.

Anyway, the discussion is going in a different direction.

That’s what I’m doing now. Learning!

At the end of the day, I’m just trying to prevent as many future struggles as humanly possible.

I know you provided one page of resources about this. Is there a “gold standard” document on API design you could point me too?

You’re input is greatly appreciated.

I don’t think there’s just a single one. But if you search Google for REST best practices, the majority of resources you’ll find all say pretty much the same thing.

Pick the one that you and your team feel most comfortable with.

1 Like