Crafting Evolvable Web API Representations
This is a modal window.
The media could not be loaded, either because the server or network failed or because the format is not supported.
Formal Metadata
Title |
| |
Title of Series | ||
Number of Parts | 163 | |
Author | ||
License | CC Attribution - NonCommercial - ShareAlike 3.0 Unported: You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal and non-commercial purpose as long as the work is attributed to the author in the manner specified by the author or licensor and the work or content is shared also in adapted form only under the conditions of this | |
Identifiers | 10.5446/50015 (DOI) | |
Publisher | ||
Release Date | ||
Language |
Content Metadata
Subject Area | ||
Genre | ||
Abstract |
|
00:00
Dependent and independent variablesTwitterMaizeFocus (optics)International Date LineTemplate (C++)WeightApplication service providerString (computer science)Product (business)Coma BerenicesData storage deviceDependent and independent variablesTwitterWeb 2.0Complex (psychology)Representation (politics)Object (grammar)Business objectDescriptive statisticsDefault (computer science)MathematicsDesign by contractWeightWeb servicePattern languageDifferent (Kate Ryan album)Serial portNamespaceBuffer overflowConstraint (mathematics)NumberSoftwareMultiplicationMereologyApplication service providerServer (computing)Product (business)InformationControl flowVariable (mathematics)Physical systemEmailString (computer science)CodeGraph (mathematics)ResultantClient (computing)Ratsche <Physik>Self-organizationGraph (mathematics)Object modelSampling (statistics)Group actionGame controllerSoftware developerProgrammer (hardware)Computing platformClosed setElement (mathematics)Formal languageGreen's functionSoftware frameworkMetadataStack (abstract data type)RootStandard deviationInterface (computing)Type theoryLocal ringGoodness of fitPlanningHypermediaTemplate (C++)Execution unitDegree (graph theory)Hardware description language.NET FrameworkFerry CorstenProcess (computing)Computer programmingPeer-to-peerRevision controlElectronic signatureCASE <Informatik>System callFile formatFocus (optics)Metropolitan area networkComputer fileMassMechanism designInternetworkingComputer animation
09:03
Application service providerWeightProduct (business)Service (economics)Stack (abstract data type)Variable (mathematics)Instance (computer science)Object (grammar)Time domainData modelControl flowProcess (computing)View (database)Standard deviationType theoryLink (knot theory)Software frameworkInformation securityWeightApplication service providerSocial classSerial portBusiness objectNumberGraph (mathematics)Pointer (computer programming)Object (grammar)Product (business)Order (biology)Cycle (graph theory)HypermediaType theoryPhysical systemMobile appSet (mathematics)Software frameworkError messageDependent and independent variablesInformation securityEndliche ModelltheorieCodeMathematicsMappingDifferent (Kate Ryan album)Domain nameConfiguration spaceLink (knot theory)Category of beingWritingWeb 2.0Object modelSubsetGame controller.NET FrameworkFigurate numberTask (computing)Java appletStandard deviationResultantWordGroup actionDefault (computer science)Mechanism designComplex (psychology)EmailAreaAttribute grammarElectronic signatureRule of inferenceConstraint (mathematics)Server (computing)Data structureSpacetimeDecision theoryRepresentation (politics)Key (cryptography)Graph (mathematics)Flow separationInterface (computing)Shape (magazine)Electronic mailing listMassGrass (card game)Latent heatState of matterRight angleGame theoryKnotView (database)Client (computing)Line (geometry)Message passingLie groupAddress spaceProcess (computing)GradientComputer animation
17:55
WeightControl flowMathematicsObject (grammar)Error messageException handlingExtension (kinesiology)Constructor (object-oriented programming)Server (computing)LengthContent (media)Type theoryRepresentation (politics)Range (statistics)Exception handlingLibrary (computing)EmailComputer wormBoolean algebraToken ringBitCore dumpPhysical systemOnline helpDesign by contractGroup actionDecision theoryFile formatOrder (biology)Client (computing)1 (number)Mechanism designRevision controlError messageData compressionMobile appMereologyProbability density functionIdentifiabilityDifferent (Kate Ryan album)Letterpress printingConnected spaceCodierung <Programmierung>InformationASCIICartesian coordinate systemPosition operatorLine (geometry)Medical imagingRepresentation (politics)Software bugCodeQuery languageMultiplicationTerm (mathematics)Serial portHash functionPhysicalismBuildingOcean currentTraffic reportingMathematicsString (computer science)CASE <Informatik>Descriptive statisticsServer (computing)Cache (computing)Process (computing)Uniform resource locatorMetadataProduct (business)Message passingElement (mathematics)Software developerLink (knot theory)Dependent and independent variablesExtension (kinesiology)Roundness (object)Streaming mediaMultiplication signAutomatic differentiationResultantWeightDisk read-and-write headFocus (optics)Content (media)Fitness functionTheoryGoodness of fitValidity (statistics)Computer clusterAverageCAN busHypermediaSymbol tableComputer animation
26:46
Group actionAttribute grammarIntegrated development environmentComputer fontPartial derivativeEmailTwitterGroup actionComputing platformCategory of beingAttribute grammarQuicksortStatement (computer science)Field (computer science)Serial portSoftware developerClient (computing)Electronic mailing listServer (computing)Object (grammar)Expected valueMultiplication signMathematicsRepresentation (politics)Order (biology)ImplementationBitMappingIsomorphieklasseType theoryIntegrated development environmentNumberHeegaard splittingDifferent (Kate Ryan album)MiniDiscError messageDefault (computer science)RootFile formatPoint (geometry)Content (media)PlanningInformationCalculationComputer configurationBuildingCASE <Informatik>Semiconductor memoryLocal ringEntire functionData structureRule of inferenceTheory of relativityEvent horizonAddress spacePresentation of a groupGame controllerSoftware testingRight angleWordPhysical systemSpherical capDescriptive statisticsGoodness of fitProjective planeLevel (video gaming)WeightCivil engineeringFlow separationTask (computing)BucklingExecution unitSoftwareSpacetimeFreewareComputer animation
35:38
EmailTwitterAttribute grammarGroup actionComputer fontLink (knot theory)HyperlinkCategory of beingSingle-precision floating-point formatActive contour modelArray data structureCommunications protocolReading (process)Multiplizität <Mathematik>Numbering schemeOntologyIdeal (ethics)Partial derivativeType theoryMultimediaMusical ensembleCASE <Informatik>Multiplication signSystem callRoundness (object)Link (knot theory)Type theoryGroup actionAddress spaceHypermediaSet (mathematics)Theory of relativityDefault (computer science)InformationDescriptive statisticsRepresentation (politics)CountingElectronic mailing listPhysical systemQuicksortCartesian coordinate systemDifferent (Kate Ryan album)NumberVariety (linguistics)Interactive televisionClient (computing)AreaOrder (biology)Field (computer science)Windows RegistryTotal S.A.TrailRow (database)MultiplicationMereologyBitServer (computing)Stability theoryCategory of beingContent (media)MetadataBuildingObject (grammar)MathematicsMultilaterationPoint (geometry)HyperlinkRootLevel (video gaming)Goodness of fitRevision controlFlow separationMoment (mathematics)Square numberArray data structureEmailOpen setRoutingDressing (medical)2 (number)Water vaporInsertion lossNetwork topologyUniformer RaumData structurePlanningString (computer science)Poisson-KlammerDependent and independent variablesSpectrum (functional analysis)Control flowFundamental theorem of algebraRight angleObject modelSingle-precision floating-point formatComputer animation
44:29
OntologyNumbering schemeIdeal (ethics)Partial derivativeType theoryMultimediaHypermediaGeneric programmingTask (computing)Computer fontMaizeEmailHyperlinkLink (knot theory)Lattice (order)Inclusion mapSource codeRevision controlComputer wormObject (grammar)Control flowMessage passingTerm (mathematics)SoftwareCategory of beingLink (knot theory)Electronic mailing listHypermediaVariety (linguistics)Revision controlBlogFile formatType theorySemantics (computer science)Group actionComputer configurationAttribute grammarSet (mathematics)Query languageDependent and independent variablesDifferent (Kate Ryan album)Social classError messageDecision theoryTwitterEntire functionMultiplication signStreaming mediaClient (computing)CASE <Informatik>Point (geometry)Physical systemRight angleCartesian coordinate systemAuthorizationNumberStandard deviationInteractive televisionLibrary (computing)CodeLimit (category theory)SubsetMultilaterationTable (information)Generic programmingIntegrated development environmentWordNamespaceMathematicsRootComputer wormControl flowProfil (magazine)Template (C++)Element (mathematics)Einbettung <Mathematik>Object (grammar)1 (number)Boom (sailing)Numbering schemeEvent horizonInversion (music)2 (number)Forcing (mathematics)Quantum stateQuicksortEscape characterLatent heatSampling (statistics)Concurrency (computer science)RoutingSoftware developerServer (computing)Data storage deviceWritingGreatest elementStrategy gameComputer clusterComputer animation
53:20
Object (grammar)Control flowMessage passingTerm (mathematics)SoftwareType theoryCategory of beingSet (mathematics)HypermediaWeb 2.0Serial portRule of inferencePhysical systemParsingFunctional (mathematics)MathematicsFeedbackView (database)Semantics (computer science)Computer programmingCartesian coordinate systemRevision controlDifferent (Kate Ryan album)Game controllerEuler anglesGeneric programmingBuildingServer (computing)Library (computing)Flow separationAlgorithmCodeObject (grammar)Metropolitan area networkState of matterElectronic data processingInjektivitätSoftwareDependent and independent variablesWeb browserTerm (mathematics)Connectivity (graph theory)Representation (politics)ParsingMereologyLatent heatBasis <Mathematik>ImplementationNumberLogicMultiplication signReal numberSoftware frameworkClient (computing)Right angleExtension (kinesiology)Computer wormMessage passingCASE <Informatik>Open sourceCharacteristic polynomialDigital photographyWeb serviceBlogInversion (music)Computer animation
01:02:12
Software developerComputer animation
Transcript: English(auto-generated)
00:03
OK, so if we can get started. Thank you very much for coming to this talk, Crafting Evolvable API Responses. My name is Darryl Miller. You can find me on Twitter. I blog fairly regularly on bizcoder.com. About all things, HTTP, API, Web APIs, REST, HyperMedia,
00:25
all that kind of stuff. I work as a developer advocate for a company called RunScope. We build tooling to help people who are building Web APIs log, monitor, and measure those Web APIs. I'm a Microsoft MVP, and I was involved as one of the co-authors of the book, Designing Evolvable Web APIs
00:43
with ASP.NET. Before I get into this talk, let's just start with a little warning. This talk is kind of like the green leafy vegetable of talks. You might not like the taste of everything that I have to say. But I can assure you that the advice is good for your
01:03
health long term. We're going to focus on API responses. There's multiple parts to API design. There's the resource design. There's representation design. We're really focusing on the representation design, which is the responses that come back when
01:22
you make an HTTP request. Now, versioning is perceived as being one solution for managing change. But it's really not the only way. And versioning is, most devs would agree, versioning can be pretty painful.
01:42
But there's another way, evolving your representations. So we're going to talk about why we think, first of all, why we think we need to do versioning, because we don't, and how we can avoid it. And well, OK, sometimes you're going to have to. So what do you do if you can't avoid it?
02:03
Part of the reason why we think we need versioning is because we tend to do this pattern. We build object graphs up on our server. And then we pass it to some infrastructure that takes care of serializing those object graphs into some wire
02:23
representation and sends it across to the other side of the wire. And some other piece of infrastructure deserializes it back into an object graph. Now, if you think about web APIs, we're talking distributed systems where the client and the server could be different languages, different platforms.
02:41
The age of the software on both sides could be completely different. Trying to build a piece of software that can generically take any arbitrary object graph, produce a byte representation that can then be deserialized with good fidelity by any other language and platform is really quite a
03:04
massive challenge. And that's why we keep writing more and more of these serializers. If you think on the .NET platform, how many are there? Data contract serializer, JSON data contract serializer, XML serializer, XAML is another serializer. We keep building more and more of these things.
03:24
But we're used to passing object graphs. It feels kind of natural to do that. But web APIs don't work the same way as local APIs. And we're in this situation. We're doing the same thing again. We've been trying to send objects over the wire for
03:42
many, many years. Corbett and DCOM had IDL, which was Interface Definition Language. And then SOAP came along and it said, we can make this easier doing this over HTTP. And we'll use WSDL as a web service description language.
04:01
And then, well, that's way too complicated. We can make this easier. We'll take this idea called REST and we'll take parts of it and we'll use JSON, because JSON's really good at object serialization. And it's so easy to do. It's so lightweight. We don't have any contracts. We don't need, wait, oh, well, we need an API
04:21
description language. We'll have, well, we don't have any organization organizers. So now we have Swagger and Rammel and API Blueprint and Waddle and Rattle. And I'm sure by next week we'll have another API description language, because people realize there needs to be some constraints in how you send information over the wire.
04:43
But a lot of this complexity that is coming back again is because we are still trying to do this pattern of sending objects over the wire. Now, when I say that, how does that manifest itself in code? So this is ASP.NET Web API, the basic default template,
05:04
File, New, Web API. And it creates this little sample controller, which is a values controller. If you've ever worked with Web API, you've probably seen this value controller. So let's look at a few things that are here in this value control. We are returning IEnumerable of string.
05:22
What does that look like on the wire? I don't know. It depends on what serializers are plugged into the framework, what accept header was passed. If it could be XML, it could be JSON. And how is that XML formatted? What are the conventions that we've defined for sending
05:41
that information over the wire? The framework decides how to do it. We absolve ourselves of any responsibility. Get string. Well, you'd think string, that should be pretty easy to serialize over the wire, right? If you've ever looked at what string serialized over the wire is in XML, it's an element called string with
06:03
this big namespace, and then the actual string inside the element, and then the close of the element. Which, as .NET programmers are like, OK, I kind of see why that is. But try showing that to a PHP developer or a Ruby developer if they'll laugh at you. Why on earth would you do that?
06:21
Just to send a simple string over the wire. In fact, they changed it in MVC6. If you do that, it just returns text slash plane as the media type with just the plain string, because they realized it was kind of silly, and it caused a lot of confusion. JSON. Wow, yeah, JSON's good at object serialization. How does JSON return that as a string?
06:43
Well, you know they actually just changed the JSON spec? The JSON spec used to say the only thing that could be at the root of a JSON document is either an array or an object. But a lot of people didn't do that. They actually put a value at the root. There are actually multiple JSON specs. There's one that's managed by ECMA, and there's another
07:00
one that's managed by the IETF. One of them said that you could put a value at the root of the document, and the other one didn't. So they've changed now. So they're at least consistent, so you can. So even within what we perceive to be a standard representation format, there's variability. And variability breaks things when you start talking between
07:22
clients and servers. Posting, a number of questions on Stack Overflow I see, because somebody just tried to post a simple string, because they didn't know they had to construct this string with namespace and XML. Now, this is possibly the world's simplest API, and I
07:41
just come up with half a dozen different interoperability issues. Can you imagine, once you get to a real API, the types of problems? And these are just primitive values. If we go into the starter tutorial, we have IEnumerable of product. You can only imagine, if product itself is an object graph, what that might look like.
08:02
And where do you go for the documentation exactly? XML serializer and Web API, does anybody know whether the default XML serializer, whether it uses data contract serializer or XML serializer? I don't remember what the default value, but they produce two completely different XML serializations
08:22
of your product's object. Now look, so that was the first one. The second one, look at this IHTP action result. How come we're not returning a domain object anymore? Well, the reason we're not returning a domain object is this motion of returning objects over the wire fails
08:41
miserably once you start wanting to affect anything in the IHTP response other than the body. In this case, we want to set the status code to either OK or not found. So because we want to change some of the metadata in the IHTP response, now all of a sudden, we have to give up
09:02
our object model serialization method, and we now have to have this different signature. And they ran into a problem with this because now their automatic documentation mechanism that generated documentation didn't work because it looked at IHTP action result and it had no idea what type of object was being returned. So now you have to annotate the method with the type of
09:22
object in order to be able to automatically generate documentation. Now I'm picking on Microsoft here for doing this, but it's not limited to Microsoft. Here's another .NET product that returns objects over the wire. It returns a list of rec stars. I wonder if it uses serialization in exactly the
09:40
same way as ASP.NET. Is there interoperability between those? Who knows? Probably not. And even the cool frameworks like Nancy, they return hello world. It's an object. We don't know what the media type is, the actual structure, we don't know the rules of that convention going across the wire. And it's not just the .NET space either.
10:01
Python returned that bunch of task objects. Now OK, at least we know specifically it's in JSON, but we don't know their conventions. Rails return an object. We just hand it off to the framework and let it do it. OK, but what is the problem with that? Why can't we just let the framework deal
10:20
with these things? There's a few questions you've got to answer, right? Let's say you are sending objects over the wire. Which objects do we send over the wire? Are we going to send our domain objects over the wire? What happens if our domain objects have some properties that we don't want to send over the wire? Oh, well it's OK. There are these magic attributes that you can
10:42
annotate all your domain objects to say this thing shouldn't go over and this. Just more complexity and more mass in your domain model that has something to do with providing interfaces to external systems. It's a poor separation of concerns.
11:00
And what happens if we want to create different views of data? The same domain model, but sometimes we want this set of data and sometimes we want this other subset of data. And of course, if we change the domain model, that changes our interface, right? So now we're going to be held back, not being able to upgrade our domain model because we have clients, external third parties, that are dependent on the
11:22
shape of that interface. So there are solutions that people have come up with this. This notion of not using DTOs or just an object specifically for the purpose of transferring a data structure over the wire. Now think about this. So in order to be able to take advantage of the automatic serialization mechanism, we're now going to
11:42
define a new set of classes in order to contain the subset of data that we need. And then we're going to write mapping code to map from our domain objects to these DTO objects so that we can take advantage of that automatic serialization code. It's a very indirect way of controlling what we go over
12:02
the wire. And I've already said, we only have limited control over what exactly that automatic serializer is doing. Automatic serialization, because what does it normally do? It'll take the object and it'll serialize out all of your properties, whether there's a
12:21
value in there or not. It'll just send out all the properties. I ran into an API. I was working with the Mandrel API. And I needed to send an email. And they said, OK, post this JSON representation up to the server.
12:40
OK, well, I need an email address, subject, and maybe a body, right? No, this had like 40 properties. And most of them I didn't have to put any value in. But I did have to put some values in some places and some default values. So I had to spend half an hour looking at the documentation to figure out all these things that I really didn't care about, because I only really wanted
13:02
to send the most basic of email. And this is a result of using some automatic serializer that assumes that all of those properties are going to be present. And it's probably going to fail if one of those properties isn't, even though it has an empty value. So I mean, what happens with the serializer if it is
13:25
missing values when you send it up? What happens if there's an unrecognized property? There's a principle that has been recognized on the web is one of the reasons the web evolves as well as it does is we have compatibility by having this must-ignore
13:43
policy, which means if you send something that I don't recognize, I'll ignore it. Because if I don't, that means that other pieces of infrastructure can't upgrade and add new capabilities without breaking me. So we have to not fail when we see unrecognized things in
14:05
order to be able to evolve. And maybe your serializer has a little configuration switch that allows it. Maybe it doesn't, but we've got to make sure that on both ends of the wire, we're doing the same thing or somebody's going to break. Null values.
14:21
Does it mean null? Does it mean unspecified? I'm building a genealogy app, right? And I've got the date of death. Fifth null, does that mean that the person is dead or not dead, or does it mean I just don't know, right? There's these significant semantic differences between missing data and data that's explicitly empty or null.
14:46
Dates and, like JSON is a good example, there's no standardized way of expressing dates in JSON. They actually went to the trouble of creating a new specification, which is based on JSON, called iJSON, for the explicit purposes of adding additional constraints to
15:05
the JSON spec so that it is truly uninteroperable. And it does define a specification for date. It does define how big a numeric value can be. And it does say, thou shalt not ever use two keys that have the same name, right?
15:21
So now we've got variants of JSON. And now I'm starting to realize there's lots of other serializers that do JSON because people now want to put comments in JSON. People don't like having to put double quotes around the property name. So there's all these flavors of JSON that are starting to appear. Empty collection and no collection.
15:42
It doesn't matter, you can do it either way. But how does your client know what to expect? Does it just assume that there's going to be an empty array there? Or is the property null? Which one is it going to be? We need to document these things, and we don't want to necessarily just leave it to the serializer to make these decisions. Capitalization issues.
16:00
Not everybody does things the same way. If we're mapping our objects directly to JSON using a serializer, how does it handle capitalization? How does it handle separating words out? Does it use underscore? Does it use hyphen? Links. If you're getting into the area of hypermedia, you want to be able to represent links in your responses you return by.
16:21
How are those going to be represented? Serializers know nothing about it, right? There's no standard object model in the .NET framework for the notion of a link. So what are they going to look like, and how do we communicate that to the consumer of our API? Cycles in graphs are great. The number of people I've heard have had production
16:41
systems that have been working for ages, no problem. Then all of a sudden, they're returning a response that's been fine for a while, and all of a sudden it's crashing out completely. Because there happens to now be in the data set a cycle in the graph of objects, and the particular serializer that they were using couldn't handle cycles. So it just crashed out, returned to 500, and then
17:02
they had to go figure out what was the problem with that. And then there's changes to the framework, like Microsoft upgrade the framework, Java upgrade it, and there are changes that happen in the underlying framework. Do you know what the impact is on all of the things that are going over the wire, and how it impacts your
17:20
third-party clients? So there's so many unknowns to be aware of and to deal with, and you can use them if you understand all of these issues, but there are a lot of issues to be aware of. You get this false sense of security with, ah, I'll just push an object, it'll come out the other side just fine
17:44
until it doesn't. Here's an example I ran into. You probably can't read the top error message. This is a little Slack app that somebody had written. And it says, error reading the integer, unexpected token
18:00
Boolean path, team.prefs.require at for mention line one position 3171. Somebody added a new preference to the preference mechanism in the API, and the client wasn't updated, and I couldn't use the client at all. It was just unusable, because one preference had been added
18:22
to one part of the app. Poor Jason.net. Is it up to version 7 now? How many different releases, constantly doing changes and bug fixes and corrections? And then he makes bug corrections, and in the
18:40
process he breaks things. Like in this particular release, it fixed the URI serialization when round tripping, and it had to do with percent encoding of values. And a friend of mine contacted me and says, all of a sudden, our API's not working anymore, because we upgraded to this version of Jason.net, and it does things differently, and ended up double encoding elements
19:04
in the string. So what am I suggesting? What am I saying is the answer. The message on the wire is your contract between the client and the server. REST does not get rid of contracts. They're still there.
19:20
We can pretend they're not, but it will bite us in the ass later on. You need to own that format and control it and understand it. You have to decide whether you want a really rigid contract that says, you must give me this, this, this, or whether you want to do a flexible contract. It's your decision to make, and don't let a third party library dictate that for you.
19:44
So, well, the problem is, if we're not going to use serializers, what are we going to do? We have this new found freedom. We've got to design our own representations. How am I going to represent the data that I want to send over the wire? So that's what we're going to do. We're going to talk about some examples and build up an
20:02
example, but before we do, let's talk about some terms and technologies, just to clear up some confusion. In the world of HTTP, there's these things called resources and representations. A resource is an incredibly nebulous term, which is basically defined as, it's some concept that is useful
20:25
to your API or your system, something. But we give it a URI. We give it an identifier. And it's the entire URI that identifies it. Well, except for the fragment identifier, right? That bit after the hash symbol, we can forget that.
20:42
The query string, yes, that's part of the URI identifier. So if we look at our examples, we have order 34 and order 35. Those are two different resources, because they have different identifiers. Order 35 format, PDF and format HTML, those are two
21:01
more different resources. Resources can be dynamic concepts, too. Print queue, current weather, leaderboard. They don't need to map to entities in the system. In fact, if you do map them to entities that are in your system, you're going to find your design very limited,
21:23
because you're going to go, well, if these are the things that we have, and the only methods we've got are get, put, post, delete, I guess pretty much the only thing we can do is CRUD, because that's all we've got. But no, resources are far more flexible things than entities.
21:42
You can do all kinds of things with resources. And a representation is the physical stream of bytes that goes over the wire that represents a resource at a snapshot in time. And resources can have multiple different representation
22:03
variants, like we can have an order that has a JSON version and a JSON-LD or a HAL version, so we can have different representations for resources. That's fine. You can also create different resources that only have a single format, like I did with the PDF and the HTML. There are two different resources.
22:21
One's HTML, one's PDF. That's perfectly valid, too. Now, if you really, really want two URLs to point to the same resource, to have a resource with two identifiers, you can do that. But ideally, it would be good if one redirects to the other
22:43
so that you don't start pollution your caches with multiple copies of the same thing that when you try and do cache invalidation, one ends up staying dirty and one's clean. So you can do it, but ideally, it would be good if you only have one URI per particular resource.
23:03
So let's talk a bit more about representations. A representation is not just the body. The representation is the entire thing that comes back from the HP response. So that's the status line with the status code. It's the headers, and it's the body. And headers are kind of cool.
23:22
Headers are kind of metadata that you can use for your own betterment. Yes, you can create new headers if you want. Just don't stick X-Stash in front of them. X-Stash didn't stand for extension. It stood for experimental. It stood for, we're just going to use this temporarily. And then when we figure out it works, we'll give it a real name.
23:41
Well, guess what happened? They didn't give it a real name because they said, no, you can't change that now. We've got all these clients that are consuming this thing that starts with X-Stash. So then we are left with all these headers that start with X-Stash. And then it became common knowledge that, oh, well, if you want to create your own custom header, stick X-Stash in front of it. No, that wasn't the intent. They actually had to go and write an RFC to tell people
24:02
not to use X-Stash because people didn't believe them. So headers, they're handy for putting metadata in there. They're handy because you can have between the client and the server intermediaries that go and look into the headers without having to parse the whole response body and they could take actions and help with your entire
24:22
layered system. And you can do clever things. You can put links into headers. So like I say, you've got some binary content, like an image, and you want links to be able to go to the next image and the previous image, you can stick those in headers, too. You can even put JSON. People are putting JSON in headers. You shouldn't really use Unicode characters.
24:41
Cams, for historical reasons, ASCII characters are the preferred. If you need to use other characters, you're going to have to use some kind of encoding. The only downside to headers is if they start getting too big, they don't get compressed. So fortunately, HTTP2 is going to help that tremendously.
25:02
As long as we keep connections open, we'll start getting some kind of header compression benefits from HTTP2. Hopefully, we'll get there soon. OK, so let's build some payloads. Well, payloads, bodies, entity, content, we have a whole range of names that we like to use for this.
25:23
And this is where we put our core application data, our semantic information. And the content-type header is an identifier in the content-type header. And that identifier tells us how to interpret what is in that body. And it tells the client, if you understand that content
25:43
type, you'll be able to understand what is in that body. So representations, we're going to use an example from the book, which is an issue tracker, because developers are familiar with issue trackers.
26:03
And this is the absolute simplest thing that we could come up with for how to represent an issue. Because, well, an issue is a user who's reporting some problem with some system. And basically, a short description is probably sufficient for some cases.
26:21
So that becomes our minimal viable product. It's our minimal representation. It makes it really easy to get something up and running. OK, it's not a full-featured issue tracker yet. But at least, in theory, we could report issues and deal with them. Having this kind of minimalistic representation
26:42
allows you to build stuff on low-capability devices, too. Especially when you start using this. I don't even have to return JSON body. I could content negotiate with text plane. And it becomes even easier. Like, maybe I could even use a watch to, I don't know.
27:04
So we need to add stuff, right? Because we need more features than just being able to have a simple description. And we're creating representation for our resource, so we're going to add some additional attributes to that resource. Here are the steps to reproduce the problem.
27:20
This is the date that we reported it. We've got to make sure that we document somewhere. This is how we format our dates so that clients know how dates are always going to be formatted in this particular type of document. But these other properties are optional.
27:41
What happens often is when you use a serializer and it returns the entire object down to the client, the client developer looks at the entire object and goes, oh, yeah, OK, cool. So I have these local client objects. I'm going to map this value to here, and this value to here, and this value to here, and this value to here. Cool, excellent. And then the server developers go, oh, yeah, wait. This property we created, we don't need that property.
28:02
Let's take it right out. And then all of a sudden, the next time that client receives the representation, it goes, I'm going to map this one to here, and this one to here. Oh, wait, it's gone. And it crashes. It fails because the property's missing. By starting out by making almost everything optional, we guide client developers to always check for presence.
28:22
Well, OK, I have got this list of properties. OK, is this presence, is this presence? In fact, what I often do is instead of checking for presence, I'll just iterate over what's there in the response and have a little switch statement. So OK, what's the first property? Oh, it's called this. OK, I put that there. Oh, this one's called this. I put that one there.
28:40
And then if it only deals with the properties that are there, and if there's something that it doesn't recognize, it just falls through to the default statement, and I can choose what to do at that point. I can choose to ignore it, or I can choose to do an error. This makes it, I mean, usually with
29:02
representations, it's fairly easy to add stuff to representations. But it's hard to remove stuff once clients start consuming it. If you always do this test for presence, then it becomes possible to remove things from your representations. Now, I've just kind of put these flat into the root of
29:23
this object. There are reasons to group attributes, and we'll talk about some of these reasons. But until you have a good reason to group them, you probably don't want to, because structural changes are hard to evolve, because there is an expectation of certain structure.
29:40
So if you can avoid structure, that's good. And this should go without saying in the JSON world is like, don't have clients depend on the order of the fields that appear. But there are XML serializers that one of them sorts them in alphabetical order, the properties.
30:00
And that's how the properties all come out, and they need to come out in sorted alphabetical order, or the deserializer won't work. Don't do that. When you're using a format, try and use its native conventions, right? There are going to be people from other platforms using it. JSON likes to use lowercase attribute names and
30:21
underscores. And because we're controlling the serialization, we can choose however we want these values to be, or these names to be presented. Use what's most natural for the format. Another question that often comes up is, well, why am I delivering this data to the user?
30:44
In particularly, I have two properties here, environment memory-free and disk space-free. I've done something different with those in that I just, it's 100 megabytes. I didn't split it out into a numeric value and a unit, because I'm pretty sure the clients that are going to be
31:03
using my app, they're just going to be presenting this on a UI. They're not going to be doing calculations of how much memory was used across these 45 different issues, and what was the average memory used. That may become a scenario further down the road, but when you build APIs, you build APIs to solve a
31:21
particular problem, and you start small, and you grow and evolve the API. There's this kind of misconception a lot of the API world, where it's like, I have some data, I'm going to expose it as an API, and they will come and do all sorts of cool things with my API. Ask Netflix and ESPN and expensify how
31:43
that went for them. They closed down their public API, because it wasn't cost justified, because they just exposed the data. And when you don't have a task-oriented API over a distributed network, it becomes very expensive to do the types of things that people want to do.
32:02
So look at your use cases and your scenarios, the types of things that people might want to do from external systems, and build scenarios around that. I know that my clients are not going to want to do calculations on that. And when they turn around and tell me that they do, then I will create some other resource in order to provide
32:21
that data in a more broken down way. Same goes for last name and first name. OK, it could be separated out. Address, do I really need to present the address in street, city, like a separate fields? Is it easier for me to format them, because different countries have different rules as to how they want to be formatted?
32:40
Is it the server should be doing that, or should it be the client doing that? You need to think about those kind of things. And just because you decided in the read-only UI, you could create writable and readable resources. So when you present it to the user, it's concatenated to last name and first name. But when you push it back up, where somebody writes
33:02
it, it could be separate fields. Don't be forced to create that kind of isomorphic type of relationship. That's the wrong word, but I think you know what I mean. So I said you can group some of this stuff, right?
33:21
So I decided we had a whole bunch of things related to environment, so I'm kind of going to group them together. It saves me a few bytes, but really, who cares about the bytes? It makes the document a little bit more pretty to read. Cool. That's always handy. And note that that is a group, so now in JSON it's an
33:40
object, but that doesn't necessarily mean it maps to any object on my server-side implementation. I've just decided for this resource representation, it makes more sense to group it like this. And there's a few reasons why I might do this. Take this particular example. I decided that I'm going to create some information
34:00
relating to who this issue is assigned to. It's assigned to Bob. Now, an issue might not be assigned to anybody, so assigned is optional. But I've decided, my business says, if it is assigned to somebody, I absolutely want to know when it was assigned.
34:21
So the date property is mandatory within the assigned group. So I started off by saying, create this absolutely minimalistic thing with just the only thing is in there is a description, and everything else an optional. Sometimes you need a little bit more control with that. And attribute groups allow you that.
34:40
They now say, if you have assigned, then we could have sub-elements of that that are mandatory. The same goes for like, you know, I have an event that's going on, and there's a start date and an end time. You might want to decide that whenever I know something about an event, I always want to know what both the start and the end time is. Or I make sure that start is always mandatory, but end is
35:03
maybe not mandatory. The other useful thing that you can do with attribute groups is you can create lists of things. It's fairly obvious. Also, you can use groups of things for pointing to
35:23
related data. Now the significance of related data is this is information about Bob. If I'm looking at the issue and editing information about the issue, I probably don't want to be changing information about Bob. So in this case, I've created a separation between my issue
35:43
related data and this group of information is about Bob. We could change who the user is, like it was reported by a different user. So we can change the whole thing, but we shouldn't be changing information within that group. So there's another reason why we might want to create a group of related data.
36:01
And that leads us to another capability, which is that we could replace that entire group by a link. Now they say, OK, yes, but now that requires two round trips for the server to be able to get that data. Yes, it does.
36:20
However, it all depends on the volatility of the data, how often data changes. You'll often get data that is highly volatile that references some data that is fairly stable. If you include that fairly stable data into the volatile
36:40
data, then every time you go back and get that volatile data, you're re-retrieving that stable data. So you're wasting bytes. With a link and some careful use of HTTP caching, you can private local caching, you can make a link, you can retrieve that stable data potentially once, and then go
37:02
back and get that volatile data multiple times. Because the link is generally always going to be less bytes over the wire than the volatile data. What you really don't ever want to do is have a big hunk of stable data with some volatile, a big customer master record with total sales to date in the field.
37:24
Because that data might be changing regularly, and it continuously invalidates the whole thing. Granularity of resources and lifetime of resources are two things that are really critical to be aware of when designing representations in order to know how much stuff
37:44
to put in your representation. Now, this particular link, I mentioned before that JSON has no standardized way of representing links. In this case, I used reported by user underscore URL.
38:01
And my client is going to have to recognize that because it ended with underscore URL, that's not just a simple string. It's actually a link to some other piece of information. And the type of link is it's a reported by user. GitHub used this style of link in their API.
38:23
There are other ways of representing links. Especially if now I have multiple links, I might want to do this way. I might want to create an array of links. And two common conventions is to use href. I mean, it's sort of coming from HTML. Use href to point to the target, and use rel to identify the type of link that I will find if I
38:45
dereference that link. I mentioned before structural changes can be painful. There's a few things that you can do to help a little bit. It's kind of good sometimes.
39:03
Whatever that application concept that you're delivering, you could create it as a property in the representation. One benefit of that is let's say you do make some breaking changes. You could always add like an issue v2 property in there
39:23
at some later point in time if you have some really fundamental breaking changes. It also allows you to maybe add a metadata property if for some reason you really don't want to put that metadata up in the headers. Same goes for array collections of things.
39:42
If you put just open square brackets, closed square brackets, and return that as a JSON response, if all of a sudden later on you decide you want to add some information about the list itself, now you can't really squeeze that in. So if you create a property called issues and make that
40:03
an array, then you could add some additional properties, maybe the count of the number of items in there, or the most recent, or there's a variety of different things that you might want to add at that root level at a later point in time. So sometimes it's just a matter of thinking ahead and planning for exactly how to structure it so that if
40:22
something happens in the future, you can deal with it. Take address. We're building a system and we need an address for somebody. And somebody comes along and says, well, yeah, but we might have multiple addresses in the future. Yeah, we might have, but we don't at the moment. So should we really build it?
40:41
Well, maybe, maybe not. Why don't we call it default address up front, and then at a later point in time, if we decide to support multiple addresses, we'll create another property called addresses with an array of addresses. So now we solve both use cases. We now have a default address, and we have the set
41:02
of all the possible addresses. So now we solve both use cases just by thinking a little bit beforehand about exactly how this was going to go over the wire. I mean, you probably could do the same thing back in your object model, but it's a bit more obvious to think about it when you're looking at it in the wire format.
41:23
So all these things that I've been talking about, do we use empty arrays or nulls, do we use single items, array of object, how we do casing, we want to take all this information and document it as part of our API document. This is our set of conventions for the documents. And we might have one set of conventions across our entire
41:42
API, or we might have multiple different types of areas that need slightly different conventions. And that's fine, because we can identify this set of conventions using that content type. The content type can say, this particular document has these semantics, and if you go read the documentation for
42:02
this media type, then it will tell you what all these conventions are. So while we're on the subject of media type, let's talk about some media types, because you deal with them when you're dealing with APIs. The large majority of people go, I'll use application slash JSON. But what does application slash JSON really tell the
42:22
client about these conventions? It tells them nothing, right? It tells them there's going to be curly braces, there's going to be properties, there's going to be objects. It doesn't really give you any more information than that. So what ends up happening is there ends up being shared knowledge between the client and the server that's not
42:41
explicit. It's what we call out of band coupling. And that's the kind of stuff that when we change it, we break it. And our only solution is to go and create a V2, because the V1 has this set of implicit consumptions, and the V2 has this set of implicit consumptions. If we're explicit about the conventions, maybe we don't
43:03
need to create a whole new version of the API. The other problem that comes along is somebody all of a sudden says, well, we're defining media types, right? OK, well, I'm building an issue tracking system, so I'm going to create my own media type for my company. This is how we deal with issue.
43:20
So there's what they call the vendor subtree and the media type registry. And it's vnd.acme.issue. And the problem is, now you're building a media type that is for you. And if that's all you care about, fine, you can do that. But if you have multiple entities in your system, now you're going to start defining media types for every
43:41
single entity in your system, and you're going to spend all your days writing specs, and people are going to hate you, and they're going to say, this is ridiculous, I'm not going to read all those specs. So there's some happy ground in the middle. In this particular case, we looked at issue, and we said, look, there are lots of issue tracking systems out there. Wouldn't it be really cool if we could have a standard
44:01
description of an issue so that there could be some interaction between different issue tracking systems? But obviously, there's political issues with getting that approved, and I don't see any movement anytime soon for the issue tracking people to take this on. But ideally, that's really the goal that we would like to get to eventually. So we can compromise, that's fine.
44:24
We could create, what I mentioned before, is we could create a set of conventions for our company, for our API. So here's application slash VND dot ACME API plus JSON. So we've said, OK, this is the media type that has all of our company's conventions. OK, that's one way of doing it. There's another set of media types, which I call them
44:45
generic media types. They're useful for many different application scenarios. They define some of the conventions, like how to present links, how to embed things in. There's a variety. We'll see a few examples. And they partially solve the problem of defining all these
45:03
conventions for you. Now, they may not suit you, but you can pick the media type that you want fits most closely to your set of conventions. But then there's a secondary thing that you apply on top of these generic hypermedia types. There's profiles and namespaces and schemas, and there's different words in different environments. But it's basically, we're layering a set of
45:22
application semantics on top of some predefined formatting semantics. So they do half the work for you. And if we look at this next slide, I highly recommend anybody who's getting involved in this is to learn from what other people have done. And here's some examples. So generic hypermedia, we've got things like HAL.
45:42
We have linked data. We have a format called Mason, Siren. Mike Amazon has Uber. There are a variety of different standards that do a good portion of the work for you. And there is ongoing work to define standardized ways of defining the other half, those application semantics.
46:04
There's also the companies that are doing the API-specific stuff. GitHub are an example. Heroku also have a media type that they've defined for their API. And that's cool, because you can write reusable code. It's great, because you're writing a client. I've written, played around with the GitHub API. And you just write a client, a decoder that follows that
46:23
set of conventions. And any GitHub document now follows that convention, and you can decode it fairly easily. In fact, because it defines conventions for links, you can actually traverse the entire GitHub API just using basically this one class that knows how to interpret all GitHub documents. And then you have other media types that are what I call
46:43
task-specific. They're designed to solve a very particular problem. I'm not really quite sure why I put JSON API in that list. It should probably go in the generic hypermedia list. So we'll skip right over that one. Atom is a media type that was defined initially for
47:00
being like for RSS readers. It was a replacement for RSS. So its purpose was to be a news feed of blog articles. And that's why it has author in there and date published and those specific types. There's things like voice XML, which is for interactive voice response type systems.
47:22
Collection plus JSON is designed for dealing with lists of things. Could be a list of anything. It's a list of things, and it gives you capabilities to query that list of things. It gives you the ability to add a new item into that list of things. And again, you can write some really nice reusable code on the client that can do with, because there's many times in an application that you're dealing with a
47:41
list of things. And at that point in time, you really don't care what that list is. Once you've selected the item in the list, maybe you care what it is. And you can follow a link and actually get specifically what is in that list. But it allows a lot of code reuse on the client. And these are built for you. Activity streams for events, kind of like the Twitter
48:00
news feed. HTTP problem. We've all written APIs, and we've all invented our own error formats for returning an error back from an API. Well, HTTP problem is a standardized one that says, this is how we should all do it. It may not be perfect, but isn't it better to use some standard library than rewriting the code all over again yourself? JSON home is a discovery document that sits at the root
48:22
of an API and tells you what other resources exist within that API. So this is HAL, a couple of samples. So we've talked about embedding resources with attribute groups. HAL specifically uses a special property name called
48:42
underscore embedded. And that tells you that everything that exists in that embedded has been brought in from some other resource. And if you dig into reported by user and go to links, and you go to the link called self, you can identify, oh, this is where this resource actually exists. We've brought this into this document to
49:02
save us round trips. We're going to embed it in, because we know we're probably going to use this data. So we've brought it in. So they've explicitly called out when you embed resources into another document. Collection plus JSON is here. You'll see they've done at the root. Instead of having an array, they have a property called
49:21
collection. Within there, there are links. So you can have links that are associated to at the root of the collection. And then in individual items, we've got data elements. This is very generic. It's name, value, pair. And the idea is that you could present this in some kind of tabular format.
49:40
And we have at the bottom, we have the notion of a query. And I didn't actually include the template element, because guess what? It's optional. You don't have to put it in there if you don't want. Now, for all of this advice, there are going to be times
50:01
when we make a mistake, we make a decision that no matter what we do, it's going to be a breaking change. And the problem is there are so many different ways that people have tried to approach versioning. And I always get asked the question, so this is my opinion on how I would approach versioning.
50:21
First of all, I would try and version the individual payload itself right within the body. That's what HTML did. It put a version number right in the document. That's what collection plus JSON does, OK? Secondly, if you're going to put it in the URI, don't
50:41
version the entire URI, because basically at that point you're saying if you put a version number in the URI, don't put it at the beginning of the URI. And you know everybody does it, but if you put it at the beginning of the URI, you're basically saying every resource that I had before, I've broken. And that's not true.
51:01
There's probably only a very small subset of the resources in your API that you've actually broken, but you've just declared that all of them are broken. And what does that do? That means your client developer, that third party poor guy, has got to make this decision. OK, I'm at V1. I've got to transition to V2.
51:21
I wonder what's broken. Now I've got to retest all the others, because you're not telling me which ones you broke. You're just saying, oh, this is a whole new V2. And you know what happens is within companies, we need to make this breaking change. Oh, this is going to version our entire API. OK, we'll hold off, and what we'll do is we'll collect all together all of our breaking changes.
51:41
And six months later, like, oh, boom, here, bang, here's the big V2. Instead of just fixing the problem immediately, they couldn't fix it immediately, because nobody wants to have the V45 API. So if you version at the end of the URI, in this case I did invoice.v2, then yeah, I'm making a breaking change
52:01
in this invoice. You can leave the regular invoice up there for all clients until you tell them, look, no, we're deprecating this. It's going to go away soon. But you can have both side by side, and the client knows that you only broke that one particular set of resources. You can also do what GitHub does. If you have a set of conventions for your entire
52:22
API, they put a V3 in their media type. You could come along and create a V4. One of the interesting things you can do with that is you can actually content negotiate. So a client can say, hey, I understand the V3 GitHub API. So send me the V3 API. And then at some later point in time, you can introduce a
52:44
V4, and when the client's ready, they can change their accept header and say, yes, now I understand the V4 API, and only then will it get it. So there's some nice negotiation things that you can do with that approach. And as I say, the last approach is maybe you really did mess up your entire API.
53:01
And then, OK, fine, put the V2 at the beginning of the API. At least you're declaring the truth. Yep, we're going to change absolutely everything here. So all I'm asking is people understand what the limitations of this objects over the wire methodology that we keep
53:21
trying to do and be aware that we've been trying to do it for a very long time. There are some scenarios it will work. If you control both ends of the wires and you can deploy both ends of the wire, the client and the server concurrently, then go at it. You're probably not going to have a problem. But this is about being able to evolve because we want
53:41
independently evolvable components. So I'm suggesting take back control over those representations you're sending over the wire. Think in terms of messages, and try and build software that is going to last. Make that part of your mentality of building software rather than, OK, let's put a V1 here because we know it's
54:02
going to be broken soon, and then we can just put V2. It's the wrong attitude. Versioning, if you believe that it's an admission of failure, then you'll think harder before you actually commit those bytes over the wire. And with that, I thank you very much. And if anybody has any questions? Yes?
54:24
A separate version for beta? No. No, because again, if we go back to that idea of the minimal viable product, like create a very small feature set straight up front, this is the type of thing that
54:40
we want our API users to do. Create a small amount of functionality to do that, and let them start using it, and start getting feedback from your users as to what else they would like to do with your API, and incrementally grow it. One of the things that needs to be really, really easy to do in any web framework you choose is it
55:00
needs to be really easy to add new resources. Because by adding new resources, you can add new features to your API, and because I'm telling you there's no one-to-one mapping between entities and resources, a resource could do anything. So evolve your API and grow it over time, but you need to grow it based on real customer feedback, and by
55:20
building something that is economical for you to support over the long term. That's my opinion. Any other questions? Yes? That is definitely an approach to create a serializer.
55:43
My only question to you would be, a lot of people have tried it, and it's still only partially successful. I mean, yes, you could define your set of conventions. And to an extent, that's what each of those generic
56:01
media types have done. The HAL, and the Uber, and collection, they've defined a set of conventions, and there's a lot of people who have gone out there and they've written libraries specifically for serializing this blob of data into a collection JSON document, or into a HAL document, or into a an Uber document.
56:21
So I guess, yes, definitely you can create a library specifically for the purpose of encoding your set of conventions. Yes, yes, it does.
56:40
Yes. The challenge is writing a serializer that can serialize any arbitrary set of application semantics. If you can limit that, like we're just looking at, say, the issue, so I have a constrained view to try and implement. If you can do that, if GitHub obviously have done it for their API, they've probably created a GitHub
57:04
serializer to serialize all responses that come back from GitHub. For RunScope, we have a set of conventions in our API, and they all RunScope documents kind of look the same, even though they contain different data. So yes, you can create a serializer with your set of
57:24
conventions. Just document them on paper, as opposed to them being purely an implementation artifact. Yes, back.
57:45
You good? Sure. If you look in the specification of a post in the HTTP spec, it says a resource can be a data processing service. That would be a perfectly viable. You've got to look at it as that credit check.
58:02
Is it safe? Is it idempotent? Is it unsafe? And based on those characteristics, pick the method that makes the most sense. If it's a credit check, you might be able to get away with just a get, because it's not actually changing any
58:20
resource state on the server. So you could just simply do a get to a resource that's a credit check. Don't get hung up on, oh, there's a verb in my URL, that particular guidance helps initially, but it's not a hard and fast rule. You can have resources that do other things.
58:42
And in the same way that if you create a method in your object that's called something really ridiculous and stupid, the system's still going to compile and your program's still going to work. If you call your URL something ridiculous and stupid, it doesn't change the system properties. It maybe makes the system less understandable, but we get
59:03
way too hung up on this. Oh, it's got a verb in it. It's not restful. Let's not go there. Any other questions? Yes.
59:25
That's an excellent question. Let's look at HTML. Yes, sorry. What's the difference between versioning the payload, putting the version number within the payload, and versioning the media type itself?
59:44
I go back to the example of text slash HTML. We haven't had a text slash HTML one, text slash HTML two, text slash HTML three. And I'm going to go out on a limb and say probably the
01:00:00
The reason for that is, as the web browser vendors have built the web browser, when new versions of HTML come out, they've built it so that the same kind of algorithm or the same set of code can process the differences between the two versions. They've handled those breaking changes using conditional logic within the media type parser.
01:00:24
Where you would use a different media type is if you wanted to discriminate. So it's like, well, if it's HTML 2 coming in, we're going to use this parsing engine, and if it's HTML 3 coming in, we're going to use a completely different parsing engine.
01:00:41
Right? Because we're going to do a kind of switch on that media type to choose what rules are going to be used to decode it. My assumption is usually the changes were probably incremental enough that it made more sense to just process it as an HTML document and deal with the differences on a case-by-case basis.
01:01:02
The man sitting on the stairs out there is the man who wrote Collection plus JSON, and he versioned Collection plus JSON in the document. So he may also be able to give you a good answer as to what are the benefits of that. But this is a good question. I can only speculate as to why they chose to do it that way.
01:01:23
But certainly it's very nice just having one text slash HTML media type and not having to deal with more. Because you think of all the... Yeah, I mean, it could have been. It was moved into doc type, right? To be able to identify the different types of HTML. For me, it limits the impact, and it keeps that versioning. I like my versioning to be as quiet as possible so people don't know I screwed up.
01:01:54
Does anybody else have any other questions? Well, thank you very much, and don't forget to press the button on the way out.