Hello everyone I'm Jeremy, lead of the WebApps Engineering team, here at Criteo, also leading the effort around our Marketing API, or MAPI if you're more intimate with it.
I wanted to talk with you about Pragmatic API Design, but that was actually an excuse to secure few minutes here and now to talk about Dungeons & Dragons !
Who here know and have already played Dungeons & Dragons, or any kind of role playing game, like Vampire, Chtulu, Warhammer, ...
For all the others, WHAT ARE YOU WAITING FOR? Come on, as you've seen there people here that could introduce you to the game. And if you're too shy, come see me after, I'd be glad to do it for you.
As there is a lot of people here, let's create an API so we can all play together, and don't wait for everyone to take their turn.
So what do I mean by pragmatic API? Don't be scared, it's **NOT** another standard, like Restfull, Hateos, GraphQL, ... because standard are generic by definition, created to cover as much uses-cases as possible, and could often make you build complex design to achieve simple things.
With Pragmatic here, I mean designing an API using our developers common sense, to create the best API possible for our specific purpose, which is in this case playing D&D. This mean we'll allow us to not follow standard, **when it make sense of course** -being pragmatic we don't want to reinvent the wheel.
Finally our key principles:
- **Affordance**: This is actually not a true english word, but I like it **"Affordance"**. It the ability of something to suggest its usage, being auto-descriptive and self-explanatory.
- **KISS**: Keep It Simple Stupid. Anyone should be able to use you API wihtout having to refer to extended documentation, **swagger must be enougth**. We're are developpers, we all do that: not looking at the documentation, and complaining if we can't understand what's happening. This is why I keep building Ikea furniture upside-down (true story, people here can relate).
- **Client centric**: Like Criteo, be client centric. Do not build the API for your data nor services. Do it for your consumers, by putting yourselves in their shoes.
- **Avoid Ambiguity**: Never allow your consumers, your clients, to do one thing with more than one way. Use standard, concrete and shared terms. Not specific business terms nor acronyms.
This is what we'll do now by playing D&D and stop talking about boring API design.
The first thing you need to play D&D is to create your own character.
Of course you can choose your name, using up to 50 letters. If you don't know why so many, you've never meet an Elvish player, and there fancy names...
There is some randomness, using Dice that you should roll to determine your abilities: Strength, Dexterity, Consitution, Intelligence, Wisdom and Charisma.
For those who wonder what's the difference between Intelligence and Wisdom: "Intelligence is knowing the Tomatoes is a fruit, Wisdom is knowing you don't put Tomatoes in a fruit salad". (Checked, I always dreamed to say that in front of many people: life achievement here).
Anyway, I do not trust my players to roll any dice, especially during character creation. So the dice rolls should be behind our API. And for simplicity, we'll not allow to distribute results amongst abilities, we'll go for full randomness.
You need to choose a race (from the Dwarf, Elf, Halfling, Human, ...) and a class (Barbarian, Paladin, Rogue, Ranger, Warlock or Wizard).
And here is how it works using our API. A basic POST request to our *characters* endpoints. And it will return the fully created character.
So, ok our Mountain Dwarf Paladin isn't that Ferocious actually, let's rename him.
But we don't want to be able to update everything, as I say I don't trust my players, change there experience, etc...
So the only thing we should allow is to PATCH our name for now.
And of course we should return the exact same result as a creation, a fully operational character.
Ok we've done the easy thing now, let's do more advanced and fun thing with our Paladin.
Usually when you play D&D, you'll very quickly found yourself fighting some ugly monsters like Kobolds, Goblins or Skeletons, or if there's a Rogue in your party, smashing the Rogue for trying to steal your money.
To attack it's simple you Roll a D20, add you Strength ability modifier (or dexterity) depending on the weapon you're using, add you're Proficiency (if your character knows how to use its weapon).
If the result is more or equal than the **ArmorClass** of your target, it's a sucess. In any case, if the dice give you a 1, it's a fumble an automatic failure and depending on you Game Master, it could end very badly, like actually hitting by mistake you're friendly wizard, who doesn't have a lot of hitpoints... But if the dice gave you a natural 20, then it's a critical an automatic success, allowing you to double your damage. But don't worry, the Game Master always find a way to take his revenge.
If you successfuly attacked, then you can hit the target, roll your weapon's damage, add your Strength (or Dexterity) and finally lower the target hitpoints.
But how do we do all of that ?
Client computation: no this is no more a simple tu use API, this will require extended works. We do not want to be Ikea.
Consequences: do we implement triggers? Reminds me too much of old SQL project. And anyway it isn't self explanatory anymore. We want to be explicit about what happens here.
Do we create new resources then, like we should with the REST approach ? Do we really want to persist them, to generate identifiers, is there any update to handle? When is it actually processed ?
Ok too many question, we're failing to design something simple, something **PRAGMATIC**.
Let's take a deep breath, and get back to D&D. What are we trying to do? We're trying to take an **ACTION**!
You will POST an action, which is not a resource, but an entity on its own.
This action, will give you an outcome (or nothing if there is no result except success/failure).
This nice thing is even if it's not a resource, we cans still nest action if we want.
And finally it's very easy to bacth actions, when they are explicitly designed.
Let's try it
This one is very easy; it doesn't need a body to be executed. We could of course create one with the weapon we want to use, ... but let's say we'll use the weapon currently equiped, so we can go grab some foods sooner.
Our action have an outcome, a bit disapointed here, as we've rolled a fumble... even if we have 6 in total thanks to our strength and proficiency (remember, D20 + strength modifier + proficiency).
So fine, let's actually hit our friendly wizard, wich is an elf as you can see by his name.
We can retreive hime easyli as usual using a GET resource if you know its Id.
We can see he only have 8 hitpoints unfortunately for him.
But anyway let's hit him.
Let's say our ferocious paladin have a Longsword equiped in his main hand. Longsword does 1d8 + his strength modifier, of slashing damage.
This is a basic action, using an HTTP POST against the wizard, fortunalty for him, it seems the wizard had already cast the spell **Blade Ward**, giving him resistance to the attack, so we only hit hime for 3 damage.
But our Paladin is Ferocious, so let's say he actually have the magic weapon: Flame Tongue!
This weapon is awesome, not only it looks badass to have a flaming weapon, but it also add 2d6 fire damage, which could overcome the wizard resistance.
In terms of API it should be 2 different hit action then, that we should execute as a **BATCH**.
So let's actually batch the action, but explicitly, by addin the explicit keyword "_batch". The underscore is important here, to make the difference between resources, actions and keywords.
It's nomenclature, starting with the underscore make it clear as sonn as you see it, that it's not an action nor a new resource, nor an the id of a resources.
And in terms of usage, it's simple too, just, add all you batch actions in an array, and receive an array of outcomes. For each action, it's index ni the array is the key to find the corresponding outcome.
Ok our Wizard is not dead, and is all out for revenge...
This is the usual wizard signature spell, and it's no joke: "A bright streak flashes from your pointing finger to an area you choose and then blossoms with a low roar into an explosion of flame."
There may be a bit of excalation betweend our Paladin and Wizard here...
"Each hit creature must make a Dexterity saving throw. Taking 8d6 fire damage on a failed save, or half as much damage on a successful one."
So we have a new kind of action here, the **Saving Throw**. It’s a test against one of your abilities. Very often you'll have to do this kind of action, to resist spell damage, or avoid being poisons, deceased...
So we have a new action, with its own outcome, once again pretty simple and explicit.
And so we can directly use it as part of the Hit action, as an optionnal **Nested** action, giving you the nested outcome.
Here we succeeded the save, and only suffer half of the rolled damage that was by then 24.
Fine, but actually... a fireball hits **EVERYONE** in the area, including our deer rogue, who was obviously trying to steal money from our Paladin.
To do that we’ll continue to use our batch keyword, but this time, in place of the id of the targeted characters. Then we can have a CharactersBatchAction, that encapsulate the action itself, and defined the ids of the character on which we want to execute the action.
That way can damage ou Paladin very badly even if he succeeded his saving throw. And as exepcted in this kind of scenario, our Rogue fumble his save, and died.
The batch weyword, can also allow us to execute in one batch different actions at the same time, here with the Hit action from the fireball, and a Constitution saving throw, because the explosion also broke some flask of poison the rogue have in its bag.
We can do even more batching !
Here, only the Paladin is doing the Constitution saving throw, because the Rogue is immune to its own poison.
Also the rogue have a special feat, allowing him to reduce incomming damage from things like a fireball, so he only suffer 4d6 fire damage. (He’s dead anyway)
Time to look out for a new character, let’s search from all the characters…
But how do we do search ?
Do we post an action to the collection?
Do we create a new SEARCH resource ?
What’s would be it’s boyd anyway?
But we know we want to return the list characters, exactly like we do on the characters endpoint.
Do we really want to build a search? Or do we actually want to add filters to the characters list ?!
Well the query string looks perfect for that ! We can even count using this way.
It require a little bit of work ou the API side, but that’s not actually that complex, to do.
In fact, some of you may have been sneaky enough to find out already, but the API is exists and is available at the URL dnd.jbuisson.fr.
The code is also fully available on gitlab, at gitlab.com/jbuisson/dnd
https://dnd.jbuisson.fr/api/characters?intelligence%3C18&intelligence%3E10&Name~=Mysterious