Client-side API Design Principles

A Collection of Interesting Ideas,

Editors:
(Microsoft)
Sangwhan Moon (Invited Expert)
Former Editor:
Domenic Denicola (Google)
By:
Members of the TAG, past and present
Participate:
GitHub w3ctag/design-principles (file an issue; open issues)

Abstract

This document contains a small-but-growing set of design principles collected by the W3C TAG while reviewing specifications.

1. JavaScript Language

1.1. Web APIs are for JavaScript

The language that web APIs are meant to be used in, and specified for, is JavaScript (also known as [ECMASCRIPT]). They are not language-agnostic, and are not meant to be.

This is sometimes a confusing point because [WEBIDL] descended from the language-agnostic OMG IDL (and at one point, included "Java Bindings"). Even today, the structure of the document contains a confusing and redundant division between the "Interface definition language" and the "ECMAScript binding". Rest assured that this division is simply a historical artifact of document structure, and does not imply anything about the intent of Web IDL in general. The only reason it remains is that nobody has taken the time to eradicate it.

As such, when designing your APIs, your primary concern should be with the interface you present to JavaScript developers. You can freely rely upon language-specific semantics and conventions, with no need to keep things generalized.

1.2. Preserve run-to-completion semantics

Web APIs are essentially vehicles for extruding C++- (or Rust-) authored capabilities into the JavaScript code that developers write. As such, it’s important to respect the invariants that are in play in normal JavaScript code. One of the most important of these is run-to-completion semantics: wherein each turn of the JavaScript event loop is processed completely before returning control to the user agent.

In particular, this means that JavaScript functions cannot be preempted mid-execution, and thus that any data observed within the function will stay constant as long as that function is active. This is not the case in other languages, which allow data races via multithreading or other techniques—a C function can be preempted at any time, with the bindings it has access to changing values from one line to the next.

This no-data-races invariant is extensively relied upon in JavaScript programs. As such, the invariant must never be violated—even by web APIs, which are often implemented in languages that do allow data races. Although the user agent may be using threads or other techniques to modify state in parallel, web APIs must never expose such changing state directly to developers. Instead, they should queue a task to modify author-observable state (such as an object property).

1.3. Do not expose garbage collection

There must not be a way for author code to deduce when/if garbage collection of JavaScript objects has run.

The reason for this is somewhat subtle. If garbage collection timing were observable, then authors could easily write code relying on specific garbage collection timing. But this timing is almost certainly not the same across user agents, which means the resulting code will be non-interoperable. Worse, according to the usual rules of game theory as applied to browsers, this kind of scenario could force other user agents to copy the garbage collection timing of the original in order to create interoperability. This would cause current garbage collection strategies to ossify, preventing improvement in one of the most dynamic areas of JavaScript virtual machine technology.

In particular, this means that you can’t expose any API that acts as a weak reference, e.g. with a property that becomes null once garbage collection runs. Such freeing of memory must be entirely deterministic.

There is some speculative discussion of exposing weak references such that their finalization is only observable between event loop turns in this stage 1 proposal under discussion at TC39. However, this remains contentious and does not yet have consensus in TC39 or among implementers.

2. API Surface Concerns

2.1. Naming things

Naming is hard! We would all like a silver-bullet for naming APIs...

Names take meaning from:

Consistency is a good principle that helps to create a platform that users can navigate intuitively and by name association.

Please consult widely on names in your APIs.

Boolean properties, options, or API parameters which are asking a question about their argument should not be prefixed with is, while methods that serve the same purpose, given that it has no side effects, should be prefixed with is to be consistent with the rest of the platform.

2.2. New features should be detectable

The existence of new features should generally be detectable, so that web content can act appropriately whether the feature is present or not. This applies both to features that are not present because they are not implemented, and to features that may not be present in a particular configuration (ranging from features that are present only on particular platforms to features that are only available in secure contexts).

It should generally be indistinguishable why a feature is unavailable, so that feature detection code written for one case of unavailability (e.g., the feature not being implemented yet in some browsers) also works in other cases (e.g., a library being used in a non-secure context when the feature is limited to secure contexts).

Detection should always be possible from script, but in some cases the feature should also be detectable in the language where it is used (such as @supports in CSS).

2.3. Attributes should behave like data properties

[WEBIDL] attributes are reified in JavaScript as accessor properties, i.e. properties with separate getter and setter functions which can react independently. This is in contrast to the "default" style of JavaScript properties, data properties, which do not have configurable behavior but instead can simply be set and retrieved, or optionally marked read-only so that they cannot be set.

Data property semantics are what are generally expected by JavaScript developers when interfacing with objects. As such, although getters and setters allow infinite customizability when defining your Web IDL attributes, you should endeavor to make the resulting accessor properties behave as much like a data property as possible. Specific guidance in this regard includes:

2.4. Consider whether objects should be live or static

Objects returned from functions, attribute getters, etc., can either be live or static. A live object is one that continues to reflect changes made after it was returned to the caller. A static object is one that reflects the state at the time it was returned.

Objects that are the way state is mutated are generally live. For example, DOM nodes are returned as live objects, since they are the API through which attributes are set and other changes to the DOM are made. They also reflect changes to the DOM made in other ways (such as through user interaction with forms).

Objects that represent a collection that might change over time (and that are not the way state is mutated) should generally be returned as static objects. This is because it is confusing to users of the API when a collection changes while being iterated. Because of this, it is generally considered a mistake that methods like getElementsByTagName() return live objects; querySelectorAll() was made to return static objects as a result of this experience. On the other hand, even though URLSearchParams represents a collection, it should be live because the collection is mutated through that object.

Note: It’s possible that some of this advice should be reconsidered for maplike and setlike types, where iterators have reasonable behavior for mutation that happens during iteration. This point likely needs further discussion, and perhaps further experience of use of these types.

It’s also worth considering the implications of having live versus static objects for the speed of implementations of the API. When the data needed by an object are expensive to compute up-front, there is an advantage for that object to be live so that the results can be computed lazily, such as for getComputedStyle(). On the other hand, if the data needed by an object are expensive to keep up-to-date, such as for the NodeList returned from querySelectorAll(), then providing a static object avoids having to keep the object updated until it is garbage collected (which may be substantially after its last use).

Likewise, the choice of live versus static objects can influence the memory use of an API. If each call of a method returns a new static object, and the objects are large, then substantial amounts of memory can be wasted until the next garbage collection.

The choice of whether an object is live or static may also influence whether it should be returned from an attribute getter or from a method. See §2.3 Attributes should behave like data properties. In particular, if a result that changes frequently is returned as a static object, it should probably be returned from a method rather than an attribute getter.

2.5. Use casing rules consistent with existing APIs

Although they haven’t always been uniformly followed, through the history of web platform API design, the following rules have emerged:

Casing rule Examples
Methods and properties
(Web IDL attributes, operations, and dictionary keys)
Camel case document.createAttribute()
document.compatMode
Classes and mixins
(Web IDL interfaces)
Pascal case NamedNodeMap
NonElementParentNode
Initialisms in APIs All caps, except when the first word in a method or property HTMLCollection
element.innerHTML
document.bgColor
Repeated initialisms in APIs Follow the same rule HTMLHRElement
RTCDTMFSender
The abbreviation of "identity" Id, except when the first word in a method or property node.getElementById()
event.pointerId
credential.id
Enumeration values Lowercase, dash-delimited "no-referrer-when-downgrade"
Events Lowercase, concatenated autocompleteerror
languagechange
HTML elements and attributes Lowercase, concatenated <figcaption>
<textarea maxlength>
JSON keys Lowercase, underscore-delimited manifest.short_name
Note that in particular, when a HTML attribute is reflected as a property, the attribute and property’s casings will not necessarily match. For example, the HTML attribute ismap on img elements is reflected as the isMap property on HTMLImageElement.

The rules for JSON keys are meant to apply to specific JSON file formats sent over HTTP or stored on disk, and don’t apply to the general notion of JavaScript object keys.

Repeated initialisms are particularly non-uniform throughout the platform. Infamous historical examples that violate the above rules are XMLHttpRequest and HTMLHtmlElement. Do not follow their example; instead always capitalize your initialisms, even if they are repeated.

2.6. Prefer dictionary parameters over boolean parameters or other unreadable parameters

APIs should generally prefer dictionary parameters (with named booleans in the dictionary) over boolean parameters. This makes the code that calls the API much more readable. It also makes the API more extensible in the future, particularly if multiple booleans are needed.

For example, new Event("exampleevent", { bubbles: true, cancelable: false}) is much more readable than new Event("exampleevent", true, false).

Furthermore, the booleans in dictionaries need to be designed so that they all default to false. If booleans default to true, then users of the API will find unexpected JavaScript behavior since { passive: false } and { passive: undefined } will produce different results. But at the same time, it’s important to avoid naming booleans in negative ways, because then code will have confusing double-negatives. These pieces of advice may sometimes conflict, but the conflict can be avoided by using opposite words without negatives, such as “repeat” versus “once”, “isolate” versus “connect”, or “private” versus “public”.

Likewise, APIs should use dictionary parameters to avoid other cases of difficult to understand sequences of parameters. For example, window.scrollBy({ left: 50, top: 0 }) is more readable than window.scrollBy(50, 0).

2.7. Design asynchronous APIs using Promises

Asynchronous APIs should generally be designed using promises rather than callback functions. This is the pattern that we’ve settled on for the Web platform, and having APIs consistently use promises means that the APIs are easier to use together (such as by chaining promises). This pattern also tends to produce cleaner code than the use of APIs with callback functions.

Furthermore, you should carefully consider whether an API might need to be asynchronous before making it a synchronous API. An API might need to be asynchronous if:

For more information on how to design APIs using promises, and on when to use promises and when not to use promises, see Writing Promise-Using Specifications.

2.8. Cancel asynchronous APIs/operations using AbortSignal

Async functions that need cancellation should take an AbortSignal as part of an options dictionary.

Example:

const controller = new AbortController();
const signal = controller.signal;
geolocation.read({ signal });

Reusing the same primitive everywhere has multiplicative effects throughout the platform. In particular, there’s a common pattern of using a single AbortSignal for a bunch of ongoing operations, and then aborting them (with the corresponding AbortController) when e.g. the user presses cancel, or a single-page-app navigation occurs, or similar. So the minor extra complexity for an individual API leads to a large reduction in complexity when used with multiple APIs together.

There might be cases where cancellation cannot be guaranteed. In these cases, the AbortController can still be used because a call to abort() on AbortController is a request to abort. How you react to it depends on your spec. Note, requestAbort() was considered in the AbortController design instead of abort(), but the latter was chosen for brevity.

2.9. Consider limiting new features to secure contexts

It may save you significant time and effort to pre-emptively restrict your feature to Secure Contexts.

The TAG is on the record in supporting an industry-wide move to Secure the Web and applaud efforts to shift web traffic to secure connections. A great deal of effort has gone into debating which features should be restricted to Secure Contexts. Opinions vary amongst engine vendors, leading to difficult choices for feature designers. Some vendors require all new features be restricted this way, whereas others take a more selective approach.

This backdrop makes it difficult to provide advice about the extent to which your feature should be restricted. What we can highlight is that Secure Context-restricted features face the least friction in gaining wide adoption amongst these varying regimes.

Specification authors can limit most features defined in WebIDL, to secure contexts by using the [SecureContext] extended attribute on interfaces, namespaces, or their members (such as methods and attributes). Similar ways of marking features as limited to secure contexts should be added to other major points where the Web platform is extended over time (for example, the definition of a new CSS property). However, for some types of extension points (e.g., dispatching an event), limitation to secure contexts should just be defined in normative prose in the specification.

As described in §2.2 New features should be detectable, the existence of features should generally be detectable, so that web content can act appropriately if the feature is present or not. Since the detection should be the same no matter why the feature is unavailable, a feature that is limited to secure contexts should, in non-secure contexts, be indistinguishable from a feature that is not implemented. However, if, for some reason (a reason that itself requires serious justification), it is not possible for developers to detect whether a feature is present, limiting the feature to secure contexts might cause problems for libraries that may be used in either secure or non-secure contexts.

If a feature would pose a risk to user privacy or security without the authentication, integrity, or confidentiality that is present only in secure contexts, then the feature must be limited to secure contexts. One example of a feature that should be limited to secure contexts is geolocation, since the authentication and confidentiality provided by secure contexts reduce the risks to user privacy. Another example is: Web USB devices grant elevated privileges to specific origins that they name, since sending untrusted data to a USB device could damage that device or compromise computers that it connects to. Thus the feature depends on the authentication of the origin and the integrity of the data, and requires secure contexts.

2.10. Constants, enums, and bitmasks

In many other platforms and programming languages, constants and enums are commonly expressed using a integer constant, sometimes in conjunction with a bitmask mechanism.

However on the Web platform, it is more common to use a string constant for the cases where a constant is needed. This is much more inspection friendly for both development and expressing the constant codes through a user facing interface, and in JavaScript engines, using integers offers no significant performance benefit over strings.

Strings do not directly address the use case for bitmasks. For these cases, you should use an object dictionary which contains the state that the bitmask is attempting to express, as object dictionaries can then be passed around from method to method as needed as easily as the state in a single bitmask value.

3. Event Design

3.1. Use promises for one time events

Follow the advice in the Writing Promise-Using Specifications guideline.

3.2. Don’t invent your own event listener-like infrastructure

For recurring events, it could be convenient to create a custom pair of APIs to "register"/"unregister", "subscribe"/"unsubscribe", etc., that take a callback and get invoked multiple times until paired cancel API is called.

Instead, use the existing event registration pattern and separate API controls to start/stop the underlying process (since event listeners should not have side-effects).

If the callback would have been provided specific data, then this data should be added to an Event object (but see State and Event subclasses as this is not always necessary).

In some cases, you can transfer the state that would be surfaced in callback parameters into a more persistent object which, in turn, can inherit from EventTarget.

In some cases you might not have an object to inherit from EventTarget but it is usually possible to create such an object.

For instance with Web Bluetooth you can add event listeners on a Characteristic object, which is obtained via getCharacteristic(). If you need to filter events, it might be possible to create a filter like

const filter = navigator.nfc.createReadFilter({
  recordType: "json"
});

const onMessage = message => {};
filter.addEventListener('exchange', onMessage);

3.3. Always add event handler attributes

For an object that inherits from EventTarget, there are two techniques available for registering an event handler (e.g., an event named "somethingchanged"):

  1. addEventListener() which allows authors to register for the event using the event’s name (i.e., someobject.addEventListener("somethingchanged", myhandler)) and

  2. onsomethingchanged IDL attributes which allow one event handler to be directly assigned to the object (i.e., someobject.onsomethingchanged).

Because there are two techniques for registering events on objects inheriting from EventTarget, authors may be tempted to omit the corresponding event handler IDL attributes. They may assume that event handler IDL attributes are a legacy registration technique or are simply not needed given that addEventListener() is available as an alternative. However, it is important to continue to define event handler IDL attributes because:

So, if the object inherits from EventTarget, add a corresponding onyourevent event handler IDL attribute to the interface.

Note that for HTML and SVG elements, it is traditional to add the event handler IDL attributes on the GlobalEventHandlers interface, instead of directly on the relevant element interface(s).

3.4. Events are for notification

Try to design DOM events to deliver after-the-fact notifications of changes. It may be tempting to try to trigger side-effects from the action of dispatchEvent(), but in general this is strongly discouraged as it requires changes to the DOM specification when added. Your design will proceed more quickly if you avoid this pattern.

3.5. Favor asynchronous events

A few events in the platform are specified to dispatch synchronously. These events cause problems for engines and performance issues in applications due to the possibility for re-entrant behavior they open up. The deprecated Mutation Events, for instance, has caused many years of security issues. A more modern approach embodied in Mutation Observers addresses most of the same use-cases in a higher-performance way which is easier to develop with and implement.

If you feel you need a synchronous event in your design, please reconsider and ask the TAG for help in redesigning the API.

3.6. State and Event subclasses

It’s tempting to create subclasses of Event for all event types. This is frequently unnecessary. Consider subclassing Event when adding unique methods and large amounts of state. In all other cases, using a "vanilla" event with state captured in the target object.

3.7. How to decide between Events and Observers

Several recent additions to the platform employ an Observer pattern. MutationObserver, IntersectionObserver, ResizeObserver, and IndexedDB Observers provide precedents for new Observer types.

Many designs can be described as either Observers or EventTargets. How to decide?

In general, start your design process using an EventTarget and only move to Observers if and when events can’t be made to work well. Using an EventTarget ensures your feature benefits from improvements to the shared base class, such as the recent addition of the once.

Observers have the following properties:

MutationObserver takes a callback which receives MutationRecords. It cannot be customized at construction time, but each observation can be customized using the MutationObserverInit set of options. It observes Nodes as targets.

IntersectionObserver takes a callback which receives IntersectionObserverEntrys. It can be customized at construction time using the IntersectionObserverInit set of options, but each observation is not further customizable. It observers Elements as targets.

Observers involve defining a new class, dictionaries for options, and a new type for the delivered records. For the cost, you gain a few advantages:

Observers and EventTargets overlap in the following ways:

Here is an example of using a hypothetical version of IntersectionObserver that is an EventTarget subclass:
const io = new ETIntersectionObserver(element, { root, rootMargin, threshold });

function listener(e) {
    for (const change of e.changes) {
        // ...
    }
}

io.addEventListener("intersect", listener);
io.removeEventListener("intersect", listener);

As you can see, we’ve lost some functionality compared to the Observer version: the ability to easily observe multiple elements with the same options, or the takeRecords() and disconnect() methods. We’re also forced to add the rather-redundant "intersect" event type to our subscription calls.

However, we haven’t lost the batching, timing, or creation-time customization, and the ETIntersectionObserver doesn’t participate in a hierarchy. These aspects can be achieved with either design.

4. Types and Units

4.1. Use numeric types appropriately

[WEBIDL] contains many numeric types. However, it is very rare that its more specific ones are actually appropriate.

JavaScript has only one numeric type, Number: IEEE 754 double-precision floating point, including ±0, ±Infinity, and NaN (although thankfully only one). The Web IDL "types" are coercion rules that apply when accepting an argument or triggering a setter. For example, a Web IDL unsigned short roughly says: "when someone passes this as an argument, take it modulo 65535 before doing any further processing". That is very rarely a useful thing to do.

Instead, you will want to stick with one of:

unrestricted double

When truly any JavaScript number will do, including infinities and NaN

double

Any JavaScript number excluding infinities and NaN

[EnforceRange] long long

Any JavaScript number in the integer-representable range, throwing a TypeError outside the range and rounding inside of it

[EnforceRange] unsigned long long

Any nonnegative JavaScript number in the integer-representable range, throwing a TypeError outside the range and rounding inside of it

Additionally, you can combine any of the above with an extra line in your algorithm to validate that the number is within the expected domain-specific range, and throwing or performing other actions in response. (While it is very rarely appropriate to modify author input by taking it modulo 65535, it might be appropriate to take it modulo 360, for example.)

A special case of domain-specific validation, which Web IDL already has you covered for, is the 0–255 range. This can be written as [EnforcedRange] octet: any JavaScript number in the range 0–255, throwing a TypeError outside the range and rounding inside of it. (And indeed, if it turns out that the other power-of-two ranges are semantically meaningful for your domain, such that you want the modulo or range-checking behavior, feel free to use them.)

Those coming from other languages should carefully note that despite their names, long long and unsigned long long only have 53 bits of precision, and not 64.

4.2. Use milliseconds for time measurement

Any web API that accepts a time measurement should do so in milliseconds. This is a tradition stemming from setTimeout and the Date API, and carried through since then.

Even if seconds (or some other unit) are more natural in the domain of an API, sticking with milliseconds ensures interoperability with the rest of the platform, allowing easy arithmetic with other time quantities.

Note that high-resolution time is usually represented as fractional milliseconds, not e.g. as nanoseconds.

4.3. Use the appropriate type to represent times and dates

When representing date-times on the platform, use the DOMTimeStamp type, with values being the number of milliseconds relative to 1970-01-01T00:00:00Z.

The JavaScript Date class must not be used for this purpose. Date objects are mutable (and there is no way to make them immutable), which comes with a host of attendant problems.

For more background on why Date must not be used, see the following:

While in theory time is monotonically increasing, values like DOMTimeStamp that represent time since 1970 are derived from the system’s clock, which may sometimes move backwards (for example, from NTP or manual adjustment), causing the timestamp values to decrease over time. They may also remain the same due to the limitation of millisecond resolution. Thus, for time stamps that do not need to correspond to an absolute time, consider using DOMHighResTimeStamp, which provides monotonically non-decreasing sub-millisecond timestamps that are comparable within a single browsing context or web worker. See [HIGHRES-TIME] for more details.

4.4. Use Error or DOMException for errors

Errors in web APIs should be represented as ECMAScript error objects (perhaps via the WebIDL Error type) or as DOMException. There was at one point a trend to use DOMError when objects had a property representing an error. However, we no longer believe there was value in this split, and therefore suggest that ECMAScript error objects (e.g., TypeError) or DOMException should be used for errors, whether they are exceptions, promise rejection values, or properties.

5. OS and Device Wrapper APIs

It is increasingly common to see new APIs developed in the web platform for interacting with devices. For example, authors wish to be able to use the web to connect with their microphones and cameras, generic sensors (such as gyroscope and accelerometer), Bluetooth and USB-connected peripherals, automobiles, toothbrush, etc. This section contains principles for consideration when designing APIs for devices.

These can be functionality provided by the underlying operating system, or provided by a native third-party library to interact with a device. These are an abstraction which "wrap" the native functionality without introducing significant complexity while securing the API surface to the browser, hence are called wrapper APIs.

5.1. Use care when exposing device identifiers

Exposing device identifiers increases the fingerprinting surface of a user agent conversely reducing the user’s privacy. Think carefully about whether it is really necessary to expose the unique identifier at all. Please read the TAG’s finding on unsanctioned tracking for additional details. Despite this general concern, it may be very useful or necessary to expose a device’s unique identifier to the web platform. The following guidelines will help ensure that this is done in a consistent and privacy-friendly way:

Limit identifiable information in the id

As much as possible, device ids exposed to the web platform should not contain identifiable information such as branding, make and model numbers, etc. In many cases using a randomized number or unique id is preferable to a generic string identifier such as "device1".

Device ids expressed as numbers should contain sufficient entropy so as to avoid re-use or potential sharing among other devices, and should not be easily guessable.

Keep the user in control

Any existing device ids mapped to or stored with the current session by the user agent should be cleared when users elect to "clear their cookies" (and other related settings). Above all, the user should be in control of this potential tracking state and be able to reset it on demand.

Hide sensitive ids behind a user permission

Where device identification does not make sense to be expressed in an anonymous way, access to the identifier should be limited by default. One way to limit exposure is to only surface the identifier to author code after obtaining permission from the user.

Tie ids to the same-origin model

Identifiers should be unique to the origin of the web content that is attempting to access them. Web content from one origin should receive an identifier that is distinct from the identifier given to web content from any other origin despite the physical device being the same.

Within an origin, ids may have a stable representation to web content. This ensures a consistent developer experience if requesting the same device twice.

Persistable when necessary

Device identifiers obtained after a complex or time-consuming device selection process may prefer that author code be allowed to persist the id for use in a later session in order to avoid the selection process a second time. In this case, the API should not only provide a stable id during the session for the given origin, but also be able to deterministically produce the same id in subsequent sessions.

5.2. Native APIs don’t typically translate well to the web

Many modern operating systems come with convenience APIs, which abstract away normally complex technology in the form of higher level user-friendly APIs.

Exposing these APIs to the web platform is getting more and more common, but you need to be careful when exposing these to the web platform.

Generalize interface when underlying API is overly specific

Don’t blindly map native convenience APIs 1:1 to the web platform. Native APIs do not translate well to the web platform, and doing so may result in a API shape which is difficult to implement across a variety of underlying platforms and native libraries.

Especially be careful about exposing the exact lifecycle and data structures of the underlying native APIs. Think about making an API that fits well in with existing web platform APIs and which can be expressed in commonly available low-level APIs.

Prefer asynchronous function by returning promises

Even if the underlying API is synchronous, this does not necessarily mean it translates well when ported to the web platform. Exposing a synchronous native API to the web platform is generally discouraged, and should be provided as a promise based API whenever possible.

Doing this also allows putting the API behind a permission if it is necessary, especially if the API depends on I/O or in the future may need to be isolated to a separate process.

Don’t overconstrain or overfit the API to the underlying (wrapped) OS

When designing a wrapper API, you should consider how different implementations (operating systems and platforms) provide this functionality. Ideally the entire specification is implementable across platforms, but in some cases it may be desirable to expose distinctions made on only some platforms.

These distinctions should be clearly noted if they are known in advance.

Do not propose black-box proprietary library or dedicated hardware processor dependencies

Black-box dependencies are strongly discouraged, as this not only prevents wide implementor adoption, but is unhealthy to the open nature of the web.

Underlying protocols should be open

APIs which require exchange with external hardware or services should not depend on closed protocols.

Be offline friendly

Web platform APIs should ideally not have a hard dependency on online services. The availability of these services across different regions may vary, users may be offline, and the online services have no guarantee of being always available.

Avoid additional fingerprinting surfaces

Wrapper APIs can expose unintentionally expose the user to a wider fingerprinting surface. Please read the TAG’s finding on unsanctioned tracking for additional details.

6. Other API Design Considerations

6.1. Privacy, Security, Accessibility, and Internationalization

It is important not to neglect other aspects of API design such as privacy, security, accessibility, and internationalization. Please take advantage of these other excellent resources in your design process:

6.2. Polyfills

Polyfills can be hugely beneficial in helping to roll out new features to the web platform. The Technical Architecture Group finding on Polyfills and the Evolution of the Web offers guidance that should be considered in the development of new features, notably:

7. Writing good specifications

This document mostly covers API design for the Web, but those who design APIs are hopefully also writing specifications for the APIs that they design.

7.1. Other resources

Some useful advice on how to write specifications is available elsewhere:

7.2. Specify clearly enough so that algorithms are unambiguous

Specification text is often more precise when it is written using an explicit sequence of steps. Using an ordered sequence of steps makes it clear how the required behaviors interact. For example, if there are multiple error conditions present, a specification that describes the behavior using a sequence of steps will make it clear which error will be reported, since the different errors would be checked and reported in different steps.

Likewise, describing state using explicit flags also makes behavior clearer. Using explicit flags makes it clear whether or not the state changes in different error conditions, and makes it clear when the state described by the flags is reset.

Whenever these differences could be observable to web content and there isn’t a good reason to allow variation between implementations, it is important that they be specified unambiguously, so that implementations interoperate and web content doesn’t end up depending on the behavior of one implementation and failing in another (which can cause higher development costs to fix the content for all browsers, or costs to users of broken content if it wasn’t detected).

However, sequences of steps are not the only way of making the specification precise enough. When there are particular characteristics that specification editors want to ensure the specification always satisfies, it may be better to use a less algorithmic way of formal specification that ensures these characteristics are always true. Developing these other ways of specifying behavior formally is more important when the algorithms would be more repetitive. These formal specification techniques can include dependencies on languages such as [WEBIDL], on grammar productions, or the development of new languages for defining pieces of the specification. Merely having a formal syntax is not sufficient, though; the requirements that the formal definitions place on implementations must still be clearly defined, which generally requires specifying some algorithms (or, sometimes, another layer of formal syntax) that define what the formal syntax means, and then using the more-readable formal syntax to avoid excessive repetition of the less-readable algorithms. There are also possibilities in-between, for example, if it is desirable to ensure that an algorithm can be implemented with a state machine, it may be preferable to specify it in an algorithmic way that incorporates explicit states so that the specification authors avoid accidentally creating more states than intended.

When specifying in an algorithmic way, it is generally good to match the algorithms that implementations would use, even if this is somewhat less convenient for specification authors, as long as doing so would not significantly increase the specification’s complexity. For example, it is better to specify CSS selector matching by moving right to left across combinators, since that is what implementations do. This avoids the risk of future problems caused by new features that would be slightly different (in unimportant ways) between implementations based on one algorithm and implementations based on a different one. Thus, it avoids small future divergence in behavior between specification and implementations. It also helps to keep the costs of specifying features aligned with implementing them, since the complexity of adding a feature to the specification is more likely to be related to the cost of adding it to implementations.

Algorithms in specifications should also reflect best practices in programming. For example, they should explictly describe the inputs and outputs of the algorithm, rather than relying on "stack introspection" or handwaving. They should also avoid side-effects when possible.

The Infra Standard offers some useful definitions and terminology for defining algorithms.

Specifications that define things using step-by-step algorithms should still be readable. Partly, this is through good practices that also apply to writing code, such as avoiding unneeded extra steps, and by using good naming. However, it’s often useful to explain the purpose of the algorithm in prose (e.g., "take the following steps, which ensure that there is at most one pending X callback per toplevel browsing context") so that readers can quickly decide whether they need to read the steps in detail.

Conformance

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CSS-CONDITIONAL-3]
CSS Conditional Rules Module Level 3 URL: https://drafts.csswg.org/css-conditional-3/
[CSSOM-1]
Simon Pieters; Glenn Adams. CSS Object Model (CSSOM). URL: https://drafts.csswg.org/cssom/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[ECMASCRIPT]
ECMAScript Language Specification. URL: https://tc39.github.io/ecma262/
[HIGHRES-TIME]
Ilya Grigorik; James Simonsen; Jatinder Mann. High Resolution Time Level 2. URL: https://w3c.github.io/hr-time/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[IndexedDB-2]
Ali Alabbas; Joshua Bell. Indexed Database API 2.0. URL: https://w3c.github.io/IndexedDB/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119
[SELECTORS-4]
Elika Etemad; Tab Atkins Jr.. Selectors Level 4. URL: https://drafts.csswg.org/selectors
[URL]
Anne van Kesteren. URL Standard. Living Standard. URL: https://url.spec.whatwg.org/
[WEBIDL]
Cameron McCormack; Boris Zbarsky; Tobie Langel. Web IDL. URL: https://heycam.github.io/webidl/
[XHR]
Anne van Kesteren. XMLHttpRequest Standard. Living Standard. URL: https://xhr.spec.whatwg.org/