5
Designing a straightforward API

This chapter covers

  • Crafting straightforward representations of concepts
  • Identifying relevant error and success feedback
  • Designing efficient usage flows

Now that you have learned to design APIs that actually let consumers achieve their goals, you have a solid foundation in API design. Unfortunately, only relying on the basics does not mean that consumers will actually be able to use the “APIs that do the job.” Remember the UDRC 1138 shown in figure 5.1? It is possible to design a terrible interface that does the job.

05-01.png

Figure 5.1 A terrible interface that does the job

When faced with an unfamiliar everyday object, what do you do? You observe it. You analyze its form, labels, icons, buttons, or other controls in order to get an understanding of its purpose, its current status, and how to operate it. To achieve your goal using this object, you might need to chain various interactions, providing inputs and receiving feedback. When you do all that, you don’t want uncertainties; everything must be crystal-clear. You don’t want to waste time, so everything must go swiftly and efficiently. You want your experience using any everyday object to be as straightforward as possible. You definitely don’t want to face another interface like the UDRC 1138. That is the basis of usability. And it’s exactly the same with APIs.

People expect APIs to be usable. They expect straightforward representations, straightforward interactions, and straightforward flows. We will uncover some fundamental principles of usability by observing everyday things in various situations, then transpose those principles to API design. We will work for now, and for the rest of this book, on an imaginary retail Banking API provided by the fictitious Banking Company. This API could be the one used, for example, by a mobile banking application to get information about current accounts, such as balance and transactions, and transfer money from one account to another. Let’s start by learning to craft representations that fit this bill.

5.1 Designing straightforward representations

How a designer chooses to represent concepts and information can greatly enhance or undermine usability. Avoiding the provider’s perspective and focusing on the consumer’s, as you learned to do in chapter 2, is the obvious first step toward usability—but we also need to take care of a few other aspects to ensure that we design straightforward representations. Let’s work on a simple alarm clock to discover these aspects.

Figure 5.2 shows how we can modify an alarm clock’s appearance. It shows how we can modify the representations used in order to make it the least usable possible.

05-02.png

Figure 5.2 Transforming an alarm clock into a less usable device

First, we can use more cryptic labels. The 24h Alarm Clock becomes the WUM 3000 (Wake Up Machine 3000). The Set Time, Set Alarm, and + and – buttons are replaced by Def Moment (Define Moment), Def Noi. Mmt (Define Noise Moment), Increm. (Increment), and Decrem. (Decrement). We can also use a less user-friendly format for the current time and replace it with the number of seconds elapsed since midnight, so 18:03 becomes 64,980. And finally, we can replace the alarm time with closely related but less useful information: a countdown to the alarm time in seconds, for example. If the current time is 18:03, the 06:00 alarm time then becomes 43,020 (and counting).

Just like the 24h Alarm Clock, the WUM 3000 does not expose inner complexity. Both are made to show time and, more importantly, make some awful noise at a given moment. But the representations used are slightly different, and this affects usability. The 24h Alarm Clock is usable because a user can understand what this device is, what its current status is, and how to use it by just reading the labels, buttons, and display screen. On the other hand, the WUM 3000 is far less usable because these elements are quite cryptic and confusing.

It’s exactly the same with APIs. The choices you make with regard to names, data formats, and data can greatly enhance or undermine an API’s usability. You already know how to design these things; you just have to consider whether the chosen representations make sense and are easily understandable for the consumer, focusing on the consumer’s perspective and designing with the user in mind. Let’s explore these topics a bit more closely, though, to fully uncover how to craft straightforward representations.

5.1.1 Choosing crystal-clear names

It’s impossible to determine what a WUM 3000 is and how to use it based solely on its name or its Def Moment and Def Noi. Mmt buttons, while a 24h Alarm Clock is obviously … an alarm clock. Code names, awkward vocabulary, and cryptic abbreviations can make an everyday object totally confusing. The same goes for APIs.

When you analyze needs with the API goals canvas, you have to name inputs and outputs. These inputs and outputs have to be represented as resources, responses, or parameters, all of which have names. These elements can contain properties, which have names too. When representing these with a tool such as the OpenAPI Specification (OAS), you might have to choose names for reusable JSON schemas. Depending on the chosen API style, goals can be represented as functions, which also have names. Names are everywhere. So how do you choose those?

In section 2.2.2, you discovered the consumer’s and provider’s perspectives. We already know that we must choose names that mean something for the consumers. But even knowing that, we must be careful when crafting those names.

Let’s say the Banking Company’s current accounts come with an optional overdraft protection feature. If the overdraft protection is active, the bank will not apply any fees in the event of the customer’s withdrawal exceeding the available balance. It could be interesting to know if this option is active or not when retrieving information about a bank account with the Banking API provided by this company. Figure 5.3 shows how it could be represented in the API.

05-03.png

Figure 5.3 Choosing a property name

The first idea is to use a boolean property named bkAccOverProtFtActBln, which is true when the feature is activated and false otherwise. But while using the Boolean type totally makes sense, this bkAccOverProtFtActBln property name is not totally user-friendly, to say the least. Using abbreviations is usually not a good idea because it makes them harder to understand. (Note that abbreviations like max or min are acceptable because they are commonly used; we’ll talk more about that in section 6.1.3.)

With fewer abbreviations, this property name becomes bankAccountOverdraftProtectionFeatureActiveBln; that’s a descriptive and readable name. But while it’s clear, it’s awfully long. Let’s see how we can find a better but still easily understandable alternative.

The Bln suffix states that this property is a Boolean. Some coding conventions can promote the use of prefixes or suffixes like bln, dto, sz, o, m_, and so on to explain the technical nature of a property, class, or structure. Because you are aware of the provider’s perspective, you’ve probably already guessed that exposing internal coding conventions in this way might not be wise. But even if we set aside the provider’s perspective, do such technical details matter to consumers? Not at all.

Consumers will have access to the API’s documentation, which describes this property as a Boolean. And when testing the API, developers will see that the property is a Boolean because its value is true or false. So, we can shorten the name to bankAccountOverdraftProtectionFeatureActive.

Because the API’s consumers can see that this property is a Boolean, we can also get rid of the Active suffix. This word is redundant with the Boolean type and, therefore, has absolutely no informative value. So, we can shorten the name to bankAccountOverdraftProtectionFeature.

Speaking of informative value, does the word Feature have any interest? Not really. From a functional point of view, it simply states that this is a property/service of the bank account. This could be explained in the property’s documentation. So, we can also get rid of this word and shorten the property’s name to bankAccountOverdraftProtection.

And finally, this property belongs to a bank account, so there’s no need to state the obvious in its name. Therefore, the property can simply be named overdraftProtection. We have gone from seven words to two.

Basically, what we did in crafting this name is to use words that consumers can understand easily, and we took advantage of the context surrounding what we are naming to find a short but still clearly understandable name. We also have avoided abbreviations and exposing internal code conventions. This is all you have to do if you want to find crystal-clear names for resources, parameters, properties, JSON schemas, or anything else that needs a name.

Some of the names we choose when designing APIs are meant to identify data. And as we saw with the overdraftProtection Boolean property, choosing adequate data types can greatly facilitate understanding.

5.1.2 Choosing easy-to-use data types and formats

The WUM 3000 showed us that inadequate data representation can ruin usability. A value such as 64,980 is not obviously a time, and even if users know it is, they still have to do some calculations to decipher its true meaning: 18:03. Lacking context, the wrong data type or format can hinder understanding and usage.

But that’s for an everyday object used by human beings. With APIs, data is simply processed by the software consuming the API, and it can perfectly interpret complex formats. It can even transform the data before showing it to an end user if necessary. So why should we care about data types and formats when designing an API?

We must never lose sight of the fact that an API is a user interface. Developers rely not only on names to understand and use the API, but also on its data. It’s common for developers to analyze sample requests and responses, call the API manually, or analyze returned data in their code for learning, testing, or debugging purposes. They might also need to manipulate specific values in their code. To do all that, they need to be able to understand the data. If the API only uses complex or cryptic data formats, such an exercise will be quite difficult. So just like names that must be understood at first sight, the meaning of an API’s raw data must always be crystal-clear for developers.

As seen in section 3.3.1, APIs typically use basic portable data types such as string, number, or boolean. It can be relatively straightforward to choose a type for a property. If we go on designing the bank account concept we started in the previous section, adding the account’s name and balance is quite simple. Indeed, a bank account’s name should obviously be a string and its balance a number. But, in some cases, you have to be careful when choosing data types and formats to ensure that what you are designing is understandable by a human being, as shown in figure 5.4. The figure shows examples of a bank account’s data in two different versions: on the left side, the data is not easy-to-use; it is the opposite on the right side.

05-04.png

Figure 5.4 Impacts of data types and format on usability

Thanks to the Date suffix and the 1534960860 value, seasoned developers should be able to understand that balanceDate is a UNIX timestamp. But will they be able to decipher this value just by reading it? Probably not. Its ISO 8601 string counterpart, "2018-08-22T18:01:00z", is far more user-friendly and can be understood by anyone without any context and without effort (well, almost without effort, once you know that it’s "YEAR-MONTH-DAY" and not "YEAR-DAY-MONTH").

The same goes for the creationDate, whose left-side value is 1423267200. But note that its ISO 8601 value shows only the date without a time: "2015-02-07". To lessen the risk of time zone mishandling, I recommend not providing a time value when it’s not needed.

The type property is supposed to tell if an account is a checking or savings account. While on the not easy-to-use side, its value is a cryptic number, 1; its easy-to-use one is a more explicit string value, checking. Using numerical codes is usually a bad idea; they are not people-friendly, so developers will have to constantly refer to the documentation or learn your nomenclature to be able to understand such data. Therefore, if you can, it is better to use data types or formats that can be understood by just reading them.

And finally, a tricky case: if the account number is provided as a number (1234567), consumers have to be careful and may have to add missing leading zeros themselves. The string value "00012345678" is easier to use and is not corrupted.

So when choosing data types and format, you must be human-friendly and, most importantly, always provide accurate representation. When using a complex format, try to provide just enough information. And, if possible, try to stay understandable without context.

OK, we know now how to choose names and data types or formats. But the usability of a representation also depends on which data we choose to use.

5.1.3 Choosing ready-to-use data

The WUM 3000 showed us how providing the wrong data can affect usability. When we design APIs, we must take care to provide relevant and helpful data, going beyond just the basic data that we learned to identify in section 3.3. The more an API is able to provide data that will aid understanding and avoid work on the consumer side, the better. Figure 5.5 shows some ways to achieve that when designing the REST API representation of the read account goal.

05-05.png

Figure 5.5 Simplify consumers work with ready-to-use data

In the previous section, you saw that it would be a better idea to provide a human-readable type like checking. But what if, for some reason, we have to use a numerical nomenclature? In that case, a savings account’s type is 1 and a checking account’s is 2. To clarify such numerical values, we can provide an additional typeName property. Its content could be savings or checking. That way the consumers will have all the information they need to understand what kind of bank account they are working with, without having to learn a nomenclature or refer to the API’s documentation. Providing additional information definitely helps to clarify cryptic data.

The overdraft protection feature has some limits. If the bank account’s balance goes into a negative value beyond a certain limit ($100, for example), fees will be applied. This means that if the balance is $500, the account’s owner can spend $600. We can provide a ready-to-use safeToSpend property in order to avoid the consumer having to do this calculation. We can also provide information about the account’s currency so consumers know that balance, overdraftLimit, and safeToSpend amounts are in US dollars. Providing static or precalculated added value data ensures that consumers have almost nothing to do or guess on their side.

It’s also a good idea to replace basic data with related but more relevant data. For example, the account’s creationDate might not really be of interest because the consumer will likely only want to know how many years the account has been open. In that case, we could provide this information directly instead of the account’s creation date. This way the consumer only gets relevant and ready-to-use data.

When we design REST APIs, each resource must be identified by a unique path (see section 3.2.3). Such URLs are usually built around resource collection names and resource identifiers. To identify a bank account, we could use the URL /accounts/{accountId}. A bank account is an item identified by an accountId in a collection named accounts. But what could this accountId be? It could be a technical ID such as 473e3283-a3b3-4941-aa48-d8163ead9ffc. This is known as a universal unique identifier (UUID). These IDs are randomly generated, and the probability of generating the same ID twice is close to zero. This way we’re sure that the /accounts/473e3283-a3b3-4941-aa48-d8163ead9ffc path is unique, but we cannot identify which bank account this URL represents by just reading it!

Maybe we could use a more user-friendly value such as the bank account number, which is also unique. That way, the URL to identify a bank account becomes /accounts/{accountNumber}. The /accounts/0001234567 path is still unique and now has a clearer meaning. Doing so is not reserved to the path parameter—providing meaningful data eases use and understanding for any value.

As you can see, once you know which aspects you must take care of, designing straightforward representations is relatively straightforward. But APIs are not only about static data: people interact with them to achieve their goals. And each one of these interactions must be straightforward too.

5.2 Designing straightforward interactions

To interact with an object or an API, users have to provide inputs to explain what they want to do. In return, they get some feedback telling them how it went. Depending on how the designer took care of the design of an interaction, these users can be completely frustrated or totally delighted. Washing machines are a perfect example of both cases.

Let’s say that you’ve just gotten back from a vacation and it’s laundry time. If you are lucky enough to have a straightforward washing machine, like the one shown on the left in figure 5.6, your task is quite simple.

05-06.png

Figure 5.6 Straightforward versus tricky washing machine

You open the door, put your laundry in, add some soap, and then choose the washing program. To do that, you turn a big knob—obviously named Program—to select the type of laundry using labels with obvious names like Wool, Silk, Cotton, Jeans, Shirts, or Synthetic. The machine will choose the appropriate clean/rinse cycles, water temperature, and spin speed according to the laundry type. It will also choose the water level according to the laundry weight given by a weight sensor. If desired, you can adjust parameters such as temperature and spin speed yourself, using the obviously named Temperature and Spin Speed buttons or knobs. But this is usually not necessary because the automatically selected parameters are accurate in 95% of all cases.

When everything is OK, you push the start button. Unfortunately, the machine does not start. It makes some alerting beeps, and the LCD screen displays a “Door still open” message. You close the door, but the machine still refuses to start; it beeps again and the LCD screen now displays a “No water” message. You forgot that you shut off the water before going on vacation! After reopening your home’s main water valve, you push the start button again. The machine now starts, and the LCD screen displays the remaining time in hours and minutes. We can even imagine that this straightforward washing machine reports the two problems in one shot to let you solve both problems at once.

Unfortunately, doing laundry is not always that simple. As shown on the right in figure 5.6, some washing machines are trickier to use.

With a not-so-straightforward washing machine, you might have to provide more information in a less user-friendly way. The programs available via the Prg knob have quite mysterious names such as Regular, Perm. Press (for permanent press), or Gent. Mot. (for gentler motion). There might be no weight sensor, and you might have to push a Half load button to indicate that the machine is not full of laundry. You might have to select the temperature and spin speed yourself using the Tmp and RPM knobs. And good luck choosing the correct temperature—from Cool to Very Hot. Once you’ve provided all the inputs, the error and success feedback might not be as informative as with the more user-friendly machine. In case of a problem such as “Door still open” or “No water,” the washing machine might simply not start. If you’re lucky, a red LED might light up, but without further explanation. And if you succeed in determining and solving all the problems, the machine might eventually start without telling you how long the wash cycle will take.

So, which type of interaction do you prefer? The straightforward one requiring minimal, understandable, and easy-to-provide inputs and showing helpful error feedback and informative success feedback? Or the tricky one requiring multiple obscure inputs and giving absolutely no clue about what is happening? This is, of course, a purely rhetorical question. Let’s see how we can transpose this idea of straightforward interactions to the design of a money transfer’s inputs and feedback for our Banking API.

Remember that in chapters 2 and 3, you learned how to design goals, parameters, and responses from the consumer’s perspective, avoiding the provider’s one. We will not discuss that matter again here.

5.2.1 Requesting straightforward inputs

The first step of an interaction belongs to the users. It’s up to them to provide some inputs to say what they want to do. As API designers, we can give them a hand by designing straightforward inputs like those on the easy-to-use washing machine, using what we learned earlier in this chapter.

In our Banking API, a money transfer consists of sending an amount of money from a source account to a destination account. The transfer can be immediate, delayed, or recurring. An immediate transfer is obviously executed immediately, while a delayed one is executed at a future date. A recurring transfer is executed multiple times, from a start date to an end date, at a set frequency (say, weekly or monthly). Figure 5.7 shows how we can apply what we have learned so far to design straightforward inputs for a money transfer. The names should be clear, avoiding obscure abbreviations; the data types and formats, easy to understand; and the data, easy to provide.

05-07.png

Figure 5.7 Designing straightforward inputs

The first of our rules for designing straightforward representations is to use crystal-clear names. The transfer money goal is therefore represented by a POST /transfers REST operation (which creates a transfer resource) instead of POST /trf (an abbreviation). We also use obvious property names like source and destination instead of src or dst, for example.

The next rule is to use easy-to-understand data formats. We will avoid the use of UNIX timestamps (1528502400, for example) and use ISO 8601 dates (2018-06-09, for example) for all date properties, such as the date that represents the delayed execution date or the first occurrence date of a recurring transfer. We also avoid using numerical codes for properties such as frequency and type, preferring instead human-readable values such as weekly or monthly and immediate, delayed, or recurring.

We can make this input even more straightforward by following the third rule and requesting easy-to-provide data. It’s better to use meaningful values such as account numbers for source and destination instead of obscure UUIDs. It also might be simpler to provide the number of money transfer occurrences instead of calculating an endDate for a recurring transfer. And finally, we can get rid of the type property that tells us if the transfer is immediate, delayed, or recurring because the backend receiving the request can guess its value based on the other properties.

This way, we end up with inputs that are totally straightforward. The user should find everything easy to understand when reading the documentation or looking at an example. And, most importantly, this API goal is dead simple to trigger. But what happens once the user has provided these straightforward inputs? Let’s explore the second part of the interaction: the feedback.

5.2.2 Identifying all possible error feedbacks

The first response we got when using the washing machine was an error feedback. What does that mean for API design? Unlike what we have seen in previous chapters, an API interaction is not always successful, and we must identify all possible errors for each goal. The transfer money goal can also trigger such error feedback, shown in figure 5.8.

05-08.png

Figure 5.8 Malformed request and functional errors

Consumers can get error feedback if they do not provide a mandatory property such as amount. They can also get an error if they provide the wrong data type or format, such as using a UNIX timestamp instead of an ISO 8601 string for the date property. Such errors are known as malformed request errors.

But even if the server is able to interpret the request, that does not guarantee the response will be a successful one. The money transfer’s amount might exceed the safe-to-spend value or the maximum amount the user is allowed to transfer in one day, or it might be forbidden to transfer money to an external account from certain internal accounts. Such errors are functional errors, triggered by the implementation’s business rules.

These malformed request and functional errors are caused by the consumers, but the provider can also trigger some even if the request is completely valid. Indeed, a down database server or a bug in the implementation can cause a server error.

That’s three different types of error—malformed, function, and server. You must identify, for each goal, all possible errors for each error type. Note that we will explore other types of errors in chapters 8 and 10.

Malformed request errors can occur when the server is unable to interpret a request. Because consumers must send requests to use the API, such errors can happen in any interaction. These errors can usually be identified just after designing the programming interface. At that point, we have a detailed view of the request that a consumer needs to send and every part of the request that might be the cause of such an error.

Functional errors mostly occur when consumers try to create, update, or delete data or trigger actions. They can typically be identified once the API goals canvas is filled in because each goal is fully described from a functional point of view. There’s no magic method to identify these potential errors; it’s up to you, helped by people who know the business rules at work behind a goal, to anticipate them. And server errors can happen on each goal. From the consumer’s perspective, identifying a single server error is usually sufficient.

When listing the errors, remember that you must always focus on the consumer’s perspective. For each of them, as discussed in section 2.4, you must check if it is the consumer’s business or not. For example, on server errors, consumers just need to know that their request could not be processed and that it is not their fault. That’s why a single generic server error is sufficient. But identifying possible errors is not enough; we must design an informative representation for each of them.

5.2.3 Returning informative error feedback

The problems encountered with the two washing machines were more or less easily solved, depending on how each error was represented. An API’s error feedback must be as informative as possible. It must explicitly tell consumers what the problem is and, if possible, provide information that consumers can use to solve it using a straightforward representation.

You saw in section 3.3.1 that a REST API that relies on the HTTP protocol uses an HTTP status code to signify if the request was a success or not, as shown in figure 5.9.

05-09.png

Figure 5.9 Choosing accurate HTTP status codes

You already saw that a 200 OK HTTP status obviously means that the processing of the request went well. If the mandatory amount is missing in the request, a 400 Bad Request status code is returned. If the amount is too high, that’s a 403 Forbidden. And if the server miserably crashed, a 500 Internal Server Error is returned.

The 200 OK set aside, how did the three other HTTP status codes come about? According to RFC 7231, which describes the “Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content,” (https://tools.ietf.org/html/rfc7231#section-6), we must use 4XX-class HTTP status codes for errors caused by the consumers and 5XX-class for errors caused by the provider.

Each class of codes contains a basic X00 code; for example, the 500 Internal Server Error is the main status of the 5XX class and is perfect to signify any type of server error in a generic way. We could use a 400 Bad Request status for all the identified errors caused by the consumers. But in that case, consumers will only know that their request is invalid without any other hint about the problem. Being able to differentiate between a “Missing mandatory amount” and “Not enough money in source account” would be quite interesting. Fortunately, the HTTP protocol comes with many 4XX codes that can be more accurate than a basic 400 Bad Request one.

We can keep the basic 400 Bad Request when there’s a missing mandatory property or an incorrect data type. To notify the user that we’re refusing to execute a transfer that exceeds the amount that’s safe to spend, or that they’ve requested a transfer they aren’t authorized to execute, we can use the 403 Forbidden status. This code means that the request is formally valid but cannot be executed.

There is another 4XX status code that you will use a lot: it is the well-known 404 Not Found, which can be used to signify that a resource is not found. For example, it could be returned on a GET /accounts/123 request if the account 123 does not exist.

There are many different HTTP status codes. You should always check which one is most accurate when designing error feedback. Table 5.1 shows how some of these can be used.

Table 5.1 Malformed request and functional error HTTP status codes

Use case Example HTTP status code
Wrong path parameter Reading a non-existing account with a GET /accounts/123 request 404 Not Found
Missing mandatory property amount not provided 400 Bad Request
Wrong data type "startDate":1423332060 400 Bad Request
Functional error amount exceeds safe to spend limit 403 Forbidden
Functional error Transfer from source to destination is forbidden 403 Forbidden
Functional error An identical money transfer has already been executed in the last 5 minutes 409 Conflict
Unexpected server error Bug in implementation 500 Internal Server Error

When consumers get one of these HTTP status codes as error feedback, they will know that the problem is on their side if the status code is a 4XX one, but they will have a better idea about the source of the problem. If the error is a 404, they will know that the provided URL does not match any existing resource and probably contains an invalid path parameter. If they get a 403, they will know that their request is formally valid but has been rejected due to some business rules. If they get a 409, they will know their request is in conflict with a previous one. And if they get a 400, they will know that their request contains invalid data or is missing a mandatory property.

That’s a good start, but an HTTP status code—even an accurate one—is not enough. The HTTP status alone does not provide enough information to help solve the problem. We should therefore also provide an explicit error message in the response body, as demonstrated in the following listing.

Listing 5.1 A basic error response body

{
  "message": "Amount is mandatory"
}

A consumer who receives a 400 Bad Request status code along with an object containing an Amount is mandatory message will be able to fix the problem easily. Well, a human consumer will be able to interpret this message easily, but what about a machine?

Let’s say our Banking API is used in a mobile application. Obviously, bluntly showing an Amount is mandatory message to an end user is better than just showing a Bad Request message. But wouldn’t it be preferable to highlight the amount field to help the end user fix the problem? How can the mobile application know which value has caused the problem? It might parse the error message string, but that would be pretty dirty. It would be better to provide a way to programmatically identify the property causing the error, as shown in the next listing.

Listing 5.2 A detailed error response

{
  "source": "amount",
  "type": "AMOUNT_OVER_SAFE",
  "message": "Amount exceeds safe to spend"
}

Along with the error message, we could provide a source property that contains the path to the property causing the problem. In this case, its value would be amount. This would enable the program to determine which value is causing the problem in the case of a malformed request. But in the event of a functional error, the mobile application still won’t know the exact type of error. Therefore, we could also add a type property containing some code. Its value for an amount exceeding what is safe for the customer to spend, for example, could be AMOUNT_OVER_SAFE. The corresponding message for the customer could be Amount exceeds safe to spend. Proceeding in this way would enable both humans and programs consuming the API to be able to accurately interpret any errors that arise.

As you can see, we have again applied the principles of straightforward representation to design these errors. Note that you don’t have to define a specific type for each error; you can define generic types as shown in the next listing. For example, the MISSING_MANDATORY_PROPERTY type can be used in any error for any missing mandatory property.

Listing 5.3 A detailed error response using a generic type

{
  "source": "amount",
  "type": "MISSING_MANDATORY_PROPERTY",
  "message": "Amount is mandatory"
}

These are only a few examples of information you can provide for errors. You are free to provide as much data as necessary in order to help consumers solve the problems themselves. You could, for instance, provide a regular expression describing the expected data format in case of a BAD_FORMAT error.

Providing informative and efficient feedback requires us to describe the problem and provide all needed information in both human- and machine-readable format in order to help the consumer solve the problem themselves (if they can). When designing a REST API, this can be done by using the appropriate HTTP status code and a straightforward response body. That works for reporting only one error at a time. But what if there are multiple problems?

5.2.4 Returning exhaustive error feedback

In the best possible scenario, the straightforward washing machine reports the two problems (door open and no water) together. This is definitely a must-have feature if you want to build usable APIs. For example, a transfer money request can present multiple malformed request errors. Suppose a customer submits a request that is missing values for the source and destination. They will first get error feedback telling them that the mandatory source property is missing. After fixing this error, they’ll do another call and get another error telling them that the destination property is missing. This is a great way to frustrate consumers. All this information could have been given in the initial error feedback!

To avoid too many request/error cycles, and the wrath of consumers, it’s best to return error feedback that is as exhaustive as possible, as shown in the next listing.

Listing 5.5 Returning multiple errors

{
  "message": "Invalid request",
  "errors": [
    {
      "source": "source",
      "type": "MISSING_MANDATORY_PROPERTY",
      "message": "Source is mandatory"},
    {
      "source": "destination",
      "type": "MISSING_MANDATORY_PROPERTY"},
      "message": "Destination is mandatory"}
  ]
}

We should therefore return in one shot a list of errors containing the two malformed request errors. Each error can be described as you saw previously in listing 5.3. The same also applies if the request is not malformed but contains multiple functional errors.

What happens if there are both types of errors? Figure 5.10 shows what might happen in that case.

05-10.png

Figure 5.10 Handling different types of errors

In this example, the request contains a valid source account but is missing the destination account and includes an amount exceeding what is safe for the user to spend. When designing single errors, we chose to use 400 Bad Request for this type of malformed request error and 403 Forbidden for the functional ones. We could choose to keep these two types of errors separated, and return error feedback to report the missing destination first, followed by a second message to report the functional error ("Amount exceeds safe to spend"). But does that make sense for consumers? Do they really care about the distinction? Probably not, at least in the specific use case where we provide all the necessary information to determine the kind of error.

I would recommend returning a generic 400 Bad Request containing all malformed request and functional errors. Note that this solution might not be the silver bullet, however. You will have to analyze your particular situation in order to make the best choice when it comes to keeping error categories separated or not.

Grouping multiple errors in one feedback message simplifies an interaction by reducing the number of request/error cycles. But if you are designing a REST API, it means using a generic HTTP status and relying on the response data to provide detailed information about each error. Once all problems are solved, the interaction should end with a success feedback.

5.2.5 Returning informative success feedback

With the washing machine use case, we saw that providing informative success feedback can be really helpful for users. It is, indeed, really helpful to known when the washing will end. Similarly, an API’s success feedback must provide useful information to the consumers beyond a simple acknowledgment. How do we achieve this? By applying what we have learned in this chapter so far!

When using the REST API style, informative success feedback can rely on the same things as error feedback: an accurate HTTP status code and a straightforward response body, as shown in figure 5.11.

05-11.png

Figure 5.11 Fully informative success feedback

As stated by RFC 7231 (https://tools.ietf.org/html/rfc7231#section-6.3), “…the 2xx (Successful) class of status code indicates that the client’s request was successfully received, understood, and accepted.” Therefore, we could return a 200 OK for all successes. But as with the 4XX class, there are many 2XX codes that can more accurately describe what has happened in some use cases.

If the money transfer is an immediate one, we could return a 201 Created HTTP status code, which means that the transfer has been created. For a delayed transfer, we could return a 202 Accepted response, indicating that the money transfer request has been accepted but not yet executed. In this case, it is implied that it will be executed at the requested date. The same goes for a recurring transfer: we could use 202 Accepted to tell the consumers that the transfers will be executed when they should. But again, an HTTP status is not enough; a straightforward and informative feedback message is far more useful.

Such a response should contain every piece of the created resource’s information, as you have learned in previous chapters. Returning properties calculated by the server (like the transfer type or its status) is interesting, just like the time at which the washing is supposed to end on the washing machine. The ID is especially interesting for consumers that might need to cancel a specific delayed transfer they have just created. Without it, they simply won’t be able to do so.

So basically, informative success feedbacks provide information about what has happened and also give information that can help during the next steps. Let’s summarize the rules we’ve identified for designing straightforward interactions:

  • Inputs and outputs must be straightforward.
  • All possible errors must be identified.
  • Error feedback must explain what the problem is and should help the consumers to solve it themselves.
  • Reporting multiple errors one by one should be avoided.
  • Success feedback should provide information about what was done and give information to help for the next steps.

We are now able to design individual interactions that are straightforward. But will these straightforward interactions form a simple flow when used together?

5.3 Designing straightforward flows

To use an object or an API, a user might have to chain multiple interactions. Usability heavily depends on the simplicity of this flow of interactions.

If you are on the 5th floor of a building and you want to go to the 16th, you might want to use one of the four elevator cabins. Figure 5.12 shows different possible versions of your elevator journey.

05-12.png

Figure 5.12 Improving elevator usage flows

If the system is basic, there’s a single call button on the wall for all cabins. It lights up when you push it. Then you wait, not knowing which one of the elevator cabins will come. A bell rings when one of them has arrived. You walk in and push the button for the 16th floor. Unfortunately, this elevator was going down to ground floor. So you go down to the ground floor and, after that, go up to the 16th.

Walking into an elevator cabin without knowing its direction of travel can be annoying. Fortunately, elevator manufacturers have enhanced their systems to avoid such a situation by adding some light signal or an LCD screen outside each elevator cabin to show if it’s going up or down. That’s better, but why stop an elevator that’s going down for someone who wants to go up? It’s really frustrating for users who are waiting for an elevator cabin, and also for the ones who are inside the cabin. This pain point can be removed by replacing the single call button with two buttons: up and down. You can now call an elevator to go up or down, and only cabins going in that direction will stop on your floor.

But when you walk into the elevator cabin, you still have to push a second button to tell it which floor you want to go to. In some systems, the up and down buttons have been replaced by the floor buttons you encounter inside the elevator cabin. Now when you want to call an elevator to go to the 16th floor, you simply push the button for that floor, and an LCD screen tells you which elevator cabin to use.

As you can see, the interaction flow to go to the 16th floor has been simplified by improving feedback, improving inputs, preventing errors, and even aggregating actions. This interaction flow has become totally straightforward. Let’s see how we can apply these principles to create a straightforward API interaction flow when transferring money with the Banking API.

5.3.1 Building a straightforward goal chain

We’ve seen how improving inputs and feedback by adding a direction indicator and replacing the call button with up and down buttons helped to improve the chain of actions needed to go to a building’s 16th floor. By taking care of inputs and feedback in a similar way in our API, we can build a straightforward goal chain.

A chain exists only if its links are connected. When consumers use an API for a specific goal, they must have all the data needed to execute it. Such data may be known by the consumers themselves or can be provided by previous goal outputs. This is what you learned in section 2.3. The partial API goals canvas shown in figure 5.13 provides such information.

05-13.png

Figure 5.13 The Banking API goals canvas

This canvas tells us that to make an immediate money transfer, consumers need to provide an amount, a source account, and a destination account. Consumers obviously know how much money they want to transfer. The source account must be one of the accounts consumers can retrieve with the list accounts goal. If the API is used by a mobile banking application, these accounts are the ones belonging to the person using the application. The destination account must be one of these accounts (for example, to transfer money from a current account to a savings account) or a pre-registered external beneficiary (for example, to send money to a friend or pay the apartment rent).

If you wonder why the Banking Company forces its customers to pre-register beneficiaries, it is for both security and usability purposes. A two-factor authentication using a regular password and a confirmation SMS, email, or security token generating random passwords is required to register an external beneficiary, thereby ensuring that only the actual customer can do so. And once the beneficiary is registered, the money transfer is quite simple: no need to remember and carefully type the destination account number and no need for two-factor authentication. Consumers can use the list account and list beneficiaries goals in order to get possible sources and destinations before they transfer money to one. So, we have a chain.

But a chain is only as strong as its weakest link. Each interaction participating in a flow must be a straightforward one. This is what you learned earlier in section 5.2. Figure 5.14 shows the transfer money flow.

05-14.png

Figure 5.14 The transfer money flow

The list accounts and list beneficiaries goals are pretty straightforward because they do not need inputs and return no errors. The inputs to the transfer money goal are straightforward, but this goal can return many different errors. If we were to only return a 400 Bad Request error message, consumers might have a hard time successfully executing a money transfer. But thanks to what you have learned in this chapter, you know now that you must provide informative and exhaustive error feedback in order to help consumers solve the problems they encounter. This will greatly reduce the number of request/error cycles and avoid artificially extending the API call chain length.

So the first step toward a straightforward API goal chain is to request simple inputs that can be provided by consumers or another goal in the chain, and return exhaustive and informative error feedback to limit request/error cycles. With what we’ve learned, we should be able to a build a straightforward goal chain. But couldn’t we make it shorter and more fluid by preventing errors?

5.3.2 Preventing errors

In the elevator example, adding a direction indicator helped to prevent an unexpected trip to the ground floor. Preventing errors is a good way to smooth and shorten the API goals flow. But how can we prevent errors? By applying one of the principles of straightforward representations—providing ready-to-use data. We have to analyze each error in order to determine if it can be prevented by providing some data prior to this goal.

The money transfer goal can trigger various functional errors:

  • Amount exceeds safe to spend limit
  • Amount exceeds cumulative daily transfer limit
  • Source account cannot be used as source for transfer
  • This destination cannot be used with this source

Let’s try to prevent the Source account cannot be used as source for transfer error. We could add a forbiddenTransfer Boolean property to each account retrieved by the list accounts goal. That way the consumer will only be able to provide a source account with this property set to false when requesting a money transfer. But it means that the consumer will have to do some filtering on its side. Figure 5.15 shows a better alternative.

05-15.png

Figure 5.15 Preventing errors in the money transfer flow

Here, a new list sources goal returning only accounts that can be used as a source for a money transfer has been added. This goal could also return with each account the maximum allowable amount for a transfer, based on the safe-to-spend and cumulative daily transfer limits, in order to prevent the corresponding errors. The consumer can use these ready-to-use values to implement surface controls on its side.

That’s quite good. With this new goal, we can prevent three of the four errors! But note that whatever error is prevented must still be handled by the transfer money goal. Some consumers cannot implement surface controls or directly call this goal without providing the proper parameters.

As you can see, preventing errors can make the goal flow more fluid. Remember that you can do this by

  • Analyzing possible errors to determine added value data that could prevent them
  • Enhancing the success feedback of existing goals to provide such data
  • Creating new goals to provide such data

Indeed, the goal flow has been improved. But is it efficient and consumer-oriented to have to call list accounts and list beneficiaries to know all the possible destination values?

5.3.3 Aggregating goals

Putting the floor buttons outside the elevator cabin permitted replacing the call an elevator and select 16th floor actions with a single one: call an elevator to go to the 16th floor. Such aggregations can be useful for optimizing the API goals flow.

It might have bothered you that the destination value could come from either the list accounts or the list beneficiaries goal. Such a design could be considered as evidence of the provider’s perspective because it requires consumers to do some work on their side. And if we take into account that some source/destination associations are forbidden, it becomes clear that this is a perfect example of the provider’s perspective. As shown in figure 5.16, we can fix that problem by creating a list destinations for source goal that replaces list accounts and list beneficiaries as the source for the destination property.

05-16.png

Figure 5.16 A single call is needed to list destinations from the selected source.

This new aggregated goal returns only the destinations possible for a given source, with the source being retrieved with list sources. This new goal will simplify the goal flow, and there’s a bonus! It also prevents the This destination cannot be used with this source error. Now consumers have fewer goals to use, and they have access to everything they need to avoid error feedback from the transfer money goal.

Is that all we can do? Figure 5.17 shows one last optimization.

05-17.png

Figure 5.17 A single call provides all the data needed to select the source and destination.

Because the number of possible source/destination combinations is relatively limited, we can provide all the possible source/destination associations with a single list sources and destinations goal that aggregates list sources and list destinations for source. It’s not mandatory, but it’s a possibility.

Be warned that such aggregations must only be done if the resulting goals really make sense for the consumer from a functional perspective. Also be warned that such aggregations can give rise to performance issues; we will talk about this subject in chapters 10 and 11. For now, there is one last thing we have to talk about to create fully straightforward flows.

5.3.4 Designing stateless flows

This topic is not present in the elevator real life example; it comes from the REST constraints you saw in section 3.5.2.

Let’s imagine, the following workflow to trigger a money transfer:

  1. List source.
  2. List destination for a selected source (the source is stored in session on the server side).
  3. Transfer $USD to destination (the source used is the one stored in session on the server).

Such flow is stateful and this is definitely not a good idea; it must never be designed nor implemented. Indeed, the transfer goal cannot be used alone as it relies on data stored in session thanks to previous calls. Some consumers might perfectly be able to choose source and destination on their own without using the list destination goal. Each goal must be usable without the others and all needed inputs must be explicitly declared.

So, in order to design totally straightforward flows, you must follow these rules:

  • Ensure that each goal provides a straightforward interaction.
  • Ensure that outputs and inputs are consistent between goal calls.
  • When possible, prevent errors by adding data to existing goals to create new goals.
  • When possible, aggregate goals but only if it make sense for the consumer from a functional perspective.
  • Each goal of the chain must be stateless.

And that’s all for straightforward API design! In the next chapter, we will continue digging into usability to learn how to design predictable APIs that can be used instinctively.

Summary

  • Any representation must be easily understandable by people and programs.
  • Any representation must be as informative as possible.
  • Error feedback must provide enough elements to understand and maybe fix the problem.
  • Success feedback must describe what has been done.
  • Goal flows can be optimized by adding data or goals to prevent errors.
  • Goal flows can be simplified by aggregating goals, but only if that makes sense from a functional perspective.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset