the ABCs and 123s of HTTP
Matt Trask
@matthewtrask
Merge PHP - 04/14/2022
Who I Am
Matt Trask
@matthewtrask
Backend Team Lead
Ovia - @oviahealth
Podcast Host
APIs You Wont Hate & PHP Townhall
OSS Maintainer
openapi.tools | phptherightway.com
What are we discussing?
Matt Trask
@matthewtrask
This is a crash course in understanding REST with a focus on Concepts, URIs, Verbs, Status Codes, Headers.
Matt Trask
@matthewtrask
![](https://s3.amazonaws.com/media-p.slid.es/uploads/392003/images/8340758/crash.jpg)
Before This Talk
Matt Trask
@matthewtrask
![](https://s3.amazonaws.com/media-p.slid.es/uploads/392003/images/8340761/mvdp.jpg)
After This Talk
What are we discussing?
Matt Trask
@matthewtrask
Concepts
The Verbs
The Status Codes
The Headers
API Contracts and Tooling
Most Importantly
Matt Trask
@matthewtrask
I want to break down the myth that REST is complicated.
What are we discussing?
Matt Trask
@matthewtrask
Concepts
URIs
The Verbs
The Status Codes
The Headers
API Contracts and Tooling
But first... some background.
Matt Trask
@matthewtrask
![](https://media2.giphy.com/media/l3fZO5GMbkHy2DIwE/giphy.gif)
The REST Pyramid of Greatness
Matt Trask
@matthewtrask
![](https://s3.amazonaws.com/media-p.slid.es/uploads/392003/images/8362303/ssss.png)
REST-ish
Matt Trask
@matthewtrask
What do we mean by REST-ish?
Anything but full Hypermedia
REST-ish
Matt Trask
@matthewtrask
GET /characters
{
"action": "POST",
"data": {
"user": {
"id": 1,
"name": "Luke Skywalker",
"affilations": [
{
"Jedi",
"Rebels",
"Resistance"
}
]
}
}
}
REST-ish
Matt Trask
@matthewtrask
GET /star-wars-characters
{
"data": {
"user": {
{
"id": 1,
"name": "Luke Skywalker",
"affilations": [
{
"Jedi",
"Rebels",
"Resistance"
}
],
"roles": [
"star pilot",
"death star killer"
],
"weapon": "Lightsaber",
"friends": [
"Obi-Wan",
"Biggs Darklighter",
"Princess Leia",
"Chewie",
"Han Solo"
]
},
{
"id": 2,
"name": "Anakin Skywalker",
"affilations": [
{
"Jedi",
"Sith",
"Empire",
"Galatic Republic"
}
],
"roles": [
"general",
"star pilot",
"pod racing",
"youngling slayer"
],
"weapon": "Lightsaber",
"friends": [
"Obi-Wan",
"Palpatine",
"Ahsoka Tano",
"R2-D2",
"Captain Rex"
]
}
}
}
}
REST-ish
Matt Trask
@matthewtrask
GET /characters
200 OK
{
"error": "There were no characters to be found"
}
What is REST?
Matt Trask
@matthewtrask
Representation State Transfer (REST)
What is REST?
Matt Trask
@matthewtrask
6 Architectural Constraints
1. Client Server Architecture
2. Statelessness
3. Cacheability
4. Layered System
5. Code On Demand (optional)
6. Uniform Interface
Theorized in a doctoral thesis by Roy Fielding in 2000.
What is REST?
Matt Trask
@matthewtrask
1. Client Server Architecture
Client and Server are independent of each other.
Allows for independent
client (iOS, Android, Web) implementations.
What is REST?
Matt Trask
@matthewtrask
2. Statelessness
Server maintains intrinsic (resource) state
Client maintains extrinsic state, and is responsible for passing state to server (filters, limits, etc)
What is REST?
Matt Trask
@matthewtrask
3. Cacheability
Clients and intermediaries can cache responses.
Responses must, implicitly or explicitly, define themselves as cacheable or non-cacheable.
What is REST?
Matt Trask
@matthewtrask
4. Layered System
Client can not discern whether it has connected to the server directly, or through an intermediary like a proxy or load balancer
What is REST?
Matt Trask
@matthewtrask
5. Code On Demand
Ability to send code on demand, such as some Javascript, to the client to be executed.
*Optional
What is REST?
Matt Trask
@matthewtrask
6. Uniform Interface
1.Resource Identification in requests
2. Resource Manipulation Through Representations
3. Self-descriptive messages
4. Hypermedia as the engine of application state
*Fundamental
Matt Trask
@matthewtrask
![](https://media1.giphy.com/media/bq6F8QYqBU7Yc/giphy.gif)
Good News!
Matt Trask
@matthewtrask
Out of those 6, the last one, mainly Hypermedia, is where most people struggle with REST
And it's super fixable!
What are we discussing?
Matt Trask
@matthewtrask
Concepts
URIs
The Verbs
The Status Codes
The Headers
API Contracts and Tooling
Concepts
Matt Trask
@matthewtrask
CORS
Hypermedia
Authentication vs Authorization
Concepts
Matt Trask
@matthewtrask
CORS
Cross Origin Resource Sharing
![](https://media4.giphy.com/media/jSWLqqAWpKmUVGuOqh/giphy.gif)
Concepts
Matt Trask
@matthewtrask
CORS
Cross Origin Resource Sharing
The bane of every single developer
Mechanism to determine is remote origin is allowed to access local server assets or data
Concepts
Matt Trask
@matthewtrask
![](https://s3.amazonaws.com/media-p.slid.es/uploads/392003/images/8376636/preflight_correct.png)
Concepts
Matt Trask
@matthewtrask
HATEOAS
Hypermedia As The Engine Of Application State
![](https://media2.giphy.com/media/1FnPDkhFZDgoU/giphy.gif)
Concepts
Matt Trask
@matthewtrask
HATEOAS
Hypermedia As The Engine Of Application State
Better described as:
Links and Discoverability
Concepts
Matt Trask
@matthewtrask
GET /api/users
200 OK
Content-type: application/json
Etag: 0bba161a7165a211c7435c950ee78438
Cache-control: max-age=3600 // 1 hour
{
"data": [
{
"id": 1,
"name": "Luke Skywalker",
"links": {
"rel": "self",
"href": "/api/users/1",
"metbod": "GET"
}
},
{
"id": 2,
"name": "Anakin Skywalker",
"links": {
"rel": "self",
"href": "/api/users/2",
"method": "GET"
}
}
]
}
Concepts
Matt Trask
@matthewtrask
GET /api/users/2
200 OK
Content-type: application/json
Etag: 0bba161a7165a211c7435c950ee78438
Cache-control: max-age=3600 // 1 hour
{
{
"id": 2,
"name": "Anakin Skywalker",
"roles": [
"general",
"star pilot",
"pod racing",
"youngling slayer"
]
"affiliation": [
"jedi",
"galatic republic",
"sith",
"empire"
],
"links": [
{
"rel": "affliation",
"href": "/api/users/2/affiliations",
"method": "GET"
},
{
"rel": "roles",
"href": "/api/users/2/roles",
"method": "GET"
},
{
"rel": "self",
"href": "/api/users/2",
"method": "PUT"
}
]
}
}
Concepts
Matt Trask
@matthewtrask
Authentication
![](https://media0.giphy.com/media/xUySTIOsf7QxHx1gk0/giphy.gif)
Concepts
Matt Trask
@matthewtrask
Authentication Strategies
HTTP Basic - username & password
API Keys - random generated string, authentication only
OAuth - handles both authentication and authorization
Concepts
Matt Trask
@matthewtrask
Authentication vs Authorization
Authentication proves you are who you say you are.
Authorization says you can do what you are trying to do.
Matt Trask
@matthewtrask
What are we discussing?
Concepts
URIs
The Verbs
The Status Codes
The Headers
API Contracts and Tooling
URIs
Matt Trask
@matthewtrask
The Entry Point to your API
Best Practices
Resource Based instead Action Based
Pluralized
Hyphened instead of underscored
URIs should describe relationships
Use UUIDs for public API ids
URIs
Matt Trask
@matthewtrask
What does resource based mean?
Resource based means that by looking at the URI, I can reasonably determine what resource I am about to consume/manipulate/remove
Also allows you to manage relationships in the resource collection
URIs
Matt Trask
@matthewtrask
GOOD https://api.starwars.com/characters
BAD https://api.starwars.com/get_characters
GOOD https://api.starwars.com/characters/ac5b605a-661e-4083-857f-d65e30e3467a
tip: you dont need to use the full uuid
BAD https://api.starwars.com/characters/1
URIs
Matt Trask
@matthewtrask
GET https://api.fancypantsbank.com/accounts
GET https://api.fancypantsbank.com/accounts/1
POST https://api.fancypantsbank.com/accounts
PUT https://api.fancypantsbank.com/accounts/1
DELETE https://api.fancypantsbank.com/accounts/1
GET https://api.fancypantsbank.com/accounts/1/savings-account
POST https://api.fancypantsbank.com/accounts/1/savings-account
PUT https://api.fancypantsbank.com/accounts/1/savings-account
DELETE https://api.fancypantsbank.com/accounts/1/savings-account
What are we discussing?
Matt Trask
@matthewtrask
Concepts
URIs
The Verbs
The Status Codes
The Headers
API Contracts and Tooling
Verbs
Matt Trask
@matthewtrask
Get
Post
Put
Patch
Delete
Options
Verbs
Matt Trask
@matthewtrask
GET
requests a representation of the specified resource
No Request Body
Response will have body
Safe
Idempotent
Cacheable
Verbs
Matt Trask
@matthewtrask
GET /api/users
200 OK
Content-type: application/json
Etag: 0bba161a7165a211c7435c950ee78438
Cache-control: max-age=3600 // 1 hour
{
"data": [
{
"id": 1,
"name": "Luke Skywalker",
"links": {
"rel": "self",
"href": "/api/users/1",
"metbod": "GET"
}
},
{
"id": 2,
"name": "Anakin Skywalker",
"links": {
"rel": "self",
"href": "/api/users/2",
"method": "GET"
}
}
]
}
Verbs
Matt Trask
@matthewtrask
Post
send data to the server to create a resource
Request has body
Response may have body
Not Safe
Not Idempotent
Cacheable If Freshness Headers is provided
Verbs
Matt Trask
@matthewtrask
POST /api/users
{
"name": "Wedge Antilles",
"species": "Corellian",
"age": 26,
"affiliations": [
"rebels",
"resistance"
],
"callsign": "Red 2"
"ship": "X-Wing"
}
201 Created
Location: /api/users/3
Content-Location: /api/users/3
Last-Modified: Wed, 24 Mar 2021 12:00:00 GMT
Verbs
Matt Trask
@matthewtrask
Put
update or create data on the server
Request will have a body
Successful response will not have body
Not Safe
Idempotent
Not cacheable
Status Codes: 201, 204
Verbs
Matt Trask
@matthewtrask
PUT /api/users/3
{
"name": "Wedge Antilles",
"species": "Corellian",
"age": 30,
"affiliations": [
"rebels",
"resistance"
],
"callsign": "Red Leader"
"ship": "X-Wing"
}
200 OK
Location: /api/users/3
Content-Location: /api/users/3
Last-Modified: Mon, 15 Nov 2017 12:00:00 GMT
Verbs
Matt Trask
@matthewtrask
Patch
partial update of a resource on a server
Request will have a body
Response may have body
Not Safe
Not Idempotent
Cacheable If Freshness Data is provided
May be restricted via headers
Status Codes: 200, 204
Verbs
Matt Trask
@matthewtrask
PATCH /api/users/3
{
"callsign": "Red Leader"
}
204 No Content
Location: /api/users/3
Content-Location: /api/users/3
Last-Modified: Wed, 24 Mar 2021 12:01:00 GMT
Verbs
Matt Trask
@matthewtrask
Delete
complete removal of a resource on a server
Request may have body
Response may have body
Not Safe
Not Idempotent
Cacheable If Freshness Data is provided
Status Codes: 200, 202, 204
Verbs
Matt Trask
@matthewtrask
DELETE /api/users/3
{
"name": "Wedge Antilles",
"species": "Corellian",
"age": 30,
"affiliations": [
"rebels",
"resistance"
],
"callsign": "Red Leader"
"ship": "X-Wing"
}
204 No Content
200 OK
{
"message": "Resource deleted",
}
Verbs
Matt Trask
@matthewtrask
Options
requests permitted communication options for a given URL or server
Request will not have body
Response will have body
Safe
Idempotent
Not cacheable
Status Codes: 204
Verbs
Matt Trask
@matthewtrask
Non-CORS
OPTIOMS /api/users
HTTP/1.1 204 No Content
Allow: OPTIONS, GET, HEAD, POST
Cache-Control: max-age=604800
Date: Wed, 24 Mar 2021 11:45:00 GMT
Server: EOS (lax004/2813)
CORS
OPTIONS https://starwars.com/api/users/ HTTP/1.1
Host: bar.example
Accept: text/html,application/xhtml+xml,application/json,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://starwars.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
What are we discussing?
Matt Trask
@matthewtrask
Concepts
URIs
The Verbs
The Status Codes
The Headers
API Contracts and Tooling
Status Codes
Matt Trask
@matthewtrask
Numbers and Phrases to give quick context for a request
// brew install httpie
http https://www.matthewtraskphotography.com
HTTP/1.1 200 OK
Status Codes
Matt Trask
@matthewtrask
100s - Informational
![](https://media3.giphy.com/media/WoWm8YzFQJg5i/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
200s - Success
![](https://media3.giphy.com/media/xNBcChLQt7s9a/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
300s - Redirection
![](https://media3.giphy.com/media/rDroB384ydCvK/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
400s - Client Errors
![](https://media1.giphy.com/media/pLawT3Jf5LHeU/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
500s - Server Error
![](https://media2.giphy.com/media/IYM94GKxUBgoU/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
They aren't Pokemon, so you dont need to catch them all.
![](https://media2.giphy.com/media/fSvqyvXn1M3btN8sDh/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
100 Continue
![](https://media0.giphy.com/media/iJnAmG5Y4klYIRl0oF/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
103 Early Hints
![](https://media1.giphy.com/media/621yHRNZD9jESngWaZ/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
200 OK
![](https://media4.giphy.com/media/ckeHl52mNtoq87veET/giphy.gif)
Appropriate Verbs: GET, PUT, PATCH, OPTIONS, DELETE
Status Codes
Matt Trask
@matthewtrask
201 Created
![](https://media2.giphy.com/media/de4QmZqpcqQMOPL2Mt/giphy.gif)
Appropriate Verbs: POST, PUT
Status Codes
Matt Trask
@matthewtrask
202 Accepted
![](https://media4.giphy.com/media/jDn8kfbWEpQShFnUAo/giphy.gif)
Appropriate Verbs: POST, PUT, PATCH, DELETE
Status Codes
Matt Trask
@matthewtrask
204 No Content
Appropriate Verbs: DELETE, GET
Status Codes
Matt Trask
@matthewtrask
206 Partial Content
![](https://media3.giphy.com/media/hmzAcor7gBsbK/giphy.gif)
not real pizza btw
Appropriate Verbs: GET
Status Codes
Matt Trask
@matthewtrask
301 Moved Permanently
![](https://media1.giphy.com/media/ieP701lvn3cCkDxFbi/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
304 Not Modified
![](https://media2.giphy.com/media/q6M0PEADcoyYJ1guuL/giphy.gif)
Headers (one must be present): Cache-control, Content-location, Etag, Date, Expires, Vary
Status Codes
Matt Trask
@matthewtrask
307 Temporary Redirect
Status Codes
Matt Trask
@matthewtrask
308 Permanent Redirect
![](https://media3.giphy.com/media/jbwNpsUxuE3p25Mf2j/giphy.gif)
Server should add a Location header
Status Codes
Matt Trask
@matthewtrask
401 Unauthorized
![](https://media3.giphy.com/media/l0HlSa0J3C7xmJDwc/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
403 Forbidden
![](https://media4.giphy.com/media/Z890hxb6fSrbG/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
406 Not Acceptable
![](https://media3.giphy.com/media/LT6BImDgVxBelOcwjH/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
409 Conflict
![](https://media3.giphy.com/media/l4JyK6uERHKGYJW8g/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
411 Length Required
![](https://media0.giphy.com/media/TvVck7lO4LDJS/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
410 Gone
![](https://media1.giphy.com/media/kHmOJ9NPzaphBWGgp5/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
413 Payload Too Large
![](https://media1.giphy.com/media/lmIqGhMyHyf7O/giphy.gif)
If time based restriction, server should send Retry-after header
Status Codes
Matt Trask
@matthewtrask
418 I'm a teapot
![](https://media0.giphy.com/media/QvRVDIiOAoASmO7QtR/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
429 Too Many Requests
![](https://media3.giphy.com/media/3s39mJ7zXU94mfAq4W/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
451 Unavailable for legal reasons
![](https://media3.giphy.com/media/fnMUG7ybQ8hwRgicfN/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
501 Not Implemented
![](https://media3.giphy.com/media/ZYECHyxt7wK92PFWVO/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
503 Service Unavailable
![](https://media4.giphy.com/media/l0HUeRyA63OreZlS0/giphy.gif)
Status Codes
Matt Trask
@matthewtrask
505 HTTP Version Not Supported
![](https://media2.giphy.com/media/eoKnetpv3cSD6/giphy.gif)
What are we discussing?
Matt Trask
@matthewtrask
Concepts
URIs
The Verbs
The Status Codes
The Headers
API Contracts and Tooling
Headers
Matt Trask
@matthewtrask
Information sent with the request and response
4 Types of Headers
General Headers
Request Headers
Response Headers
Entity Headers
Headers
Matt Trask
@matthewtrask
Accept | Content-type
# Request
Accept: application/json
Accept: application/vnd.github+json
Accept: application/xml
Accept: text/plain
Accept: text/csv
# Response
Content-type: application/json
Content-type: text/html
Content-type: text/csv
Content-type: application/vnd.github+json
These headers allow for content negotiation: where the server decides between the mime types which is best to send
Headers
Matt Trask
@matthewtrask
Authorization
# Request
Authorization: Bearer 5eb63bbbe01eeed093cb22bb8f5acdc3
Authorization: Basic a94a8fe5ccb19ba61c4c0873d391e987982fbbd3
Headers
Matt Trask
@matthewtrask
Cache-Control
# Request
Cache-Control: max-age=<seconds>
Cache-Control: max-stale[=<seconds>]
Cache-Control: min-fresh=<seconds>
Cache-Control: no-cache
Cache-Control: no-store
Cache-Control: no-transform
Cache-Control: only-if-cached
# Response
Cache-Control: no-cache
Cache-Control: no-store
Cache-Control: no-transform
Cache-Control: public
Cache-Control: private
Cache-Control: proxy-revalidate
Cache-Control: max-age=<seconds>
Cache-Control: s-maxage=<seconds>
Headers
Matt Trask
@matthewtrask
Location
# Response
Content-Location: /api/characters/3
Location: /api/characters/3
Headers
Matt Trask
@matthewtrask
Etag | Age | Last-Modified
# Response
Etag: 5eb63bbbe01eeed093cb22bb8f5acdc3
Age: 60 // in seconds
Last-modified: Wed, 24 Mar 2021-03-24 12:00:00 GMT // “HTTP-date” format as defined by RFC 7231
Headers
Matt Trask
@matthewtrask
Content-length | Content-language | Expires
# Response
Content-length: 128 // represented by 8-bit bytes
Content-language: en
Expires: Tue, 29 Mar 2021 23:59:59 GMT
Headers
Matt Trask
@matthewtrask
CORS
Access-Control-Allow-Origin
Access-Control-Allow-Credentials
Access-Control-Expose-Headers
Access-Control-Max-Age
Access-Control-Allow-Methods
Access-Control-Allow-Headers
What are we discussing?
Matt Trask
@matthewtrask
Concepts
URIs
The Verbs
The Status Codes
The Headers
API Contracts and Tooling
API Contracts
Matt Trask
@matthewtrask
API Contracts
OpenAPI
JSON Schema
Tooling
Resources
API Contracts
Matt Trask
@matthewtrask
What Is An API Contract?
It is something that both API provider and API consumer can agree upon, and get to work developing and delivering, and then integrating and consuming. An API contract is a shared understanding of what the capabilities of a digital interface are, allowing for applications to be programmed on top of.
OpenAPI
Matt Trask
@matthewtrask
A description language that allows for documentation generation, testing, SDK generation, and more.
The resulting file creates the "contract" that both the server and client can reliable consume.
The artist formerly known as Swagger
OpenAPI
Matt Trask
@matthewtrask
![](https://s3.amazonaws.com/media-p.slid.es/uploads/392003/images/9001857/Screen_Shot_2021-10-10_at_8.25.52_AM.png)
OpenAPI
Matt Trask
@matthewtrask
![](https://s3.amazonaws.com/media-p.slid.es/uploads/392003/images/5865064/OpenAPIDrivenDevelopment.png)
OpenAPI
Matt Trask
@matthewtrask
![](https://s3.amazonaws.com/media-p.slid.es/uploads/392003/images/8381681/Screen_Shot_2021-03-23_at_9.54.43_PM.png)
https://github.com/matthewtrask/national-parks-api/blob/main/contract/openapi.yml
OpenAPI
Matt Trask
@matthewtrask
![](https://s3.amazonaws.com/media-p.slid.es/uploads/392003/images/8381684/Screen_Shot_2021-03-23_at_9.55.48_PM.png)
JSON Schema
Matt Trask
@matthewtrask
JSON Schema is a vocabulary that allows you to annotate and validate JSON documents.
Matt Trask
@matthewtrask
{
"$id": "https://example.com/address.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "An address similar to http://microformats.org/wiki/h-card",
"type": "object",
"properties": {
"post-office-box": {
"type": "string"
},
"extended-address": {
"type": "string"
},
"street-address": {
"type": "string"
},
"locality": {
"type": "string"
},
"region": {
"type": "string"
},
"postal-code": {
"type": "string"
},
"country-name": {
"type": "string"
}
},
"required": [ "locality", "region", "country-name" ],
"dependentRequired": {
"post-office-box": [ "street-address" ],
"extended-address": [ "street-address" ]
}
}
OpenAPI vs JSON Schema
Matt Trask
@matthewtrask
OpenAPI 3.1 finally worked through some minor issues and now OpenAPI and JSON Schema play nice together!
![](https://media4.giphy.com/media/30ogPwml4Am2I/giphy.gif)
Tooling
Matt Trask
@matthewtrask
Postman - HTTP Testing
Paw (Mac Only) - HTTP Testing
HTTPie (CLI) - HTTP Testing
Insomnia - HTTP Testing
Postwoman - HTTP Testing
Stoplight Studio - Documentation
Stoplight Spectral - Linting
Dredd.io - Testing
jq - CLI JSON map/filter/slice
Resources
Matt Trask
@matthewtrask
APIs You Wont Hate (book, blog, podcast)
API Evangelist (blog)
Nordic APIs (blog, events)
People to follow:
Phil Sturgeon (OpenAPI/Loud Person) @philsturgeon
Ben Hutton (JSON Schema) @benhutton
Kin Lane @apievangelist
Darrel Miller (OpenAPI TSC) @darrelmiller
Mike Amundsen @mamund
Arnaud Lauret @apihandyman
Mike Ralphson (OpenAPI TSC) @permittedsoc
Thank you!
Matt Trask
@matthewtrask
@matthewtrask | mjftrask@gmail.com
podcast: apis you wont hate
apisyouwonthate.com | matthewtrask.net
Merge PHP - API Crash Course
By Matt Trask
Merge PHP - API Crash Course
- 156