Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
APIs with Laravel - Laracon 2018
Search
TJ
July 26, 2018
Programming
3.6k
6
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
APIs with Laravel - Laracon 2018
TJ
July 26, 2018
Other Decks in Programming
See All in Programming
SlackアプリとLambdaの 連携を構築した話
pawn_4_s
1
130
Detecting Compromised CI with eBPF and Cilium Tetragon
lizrice
0
220
言葉の格闘技のススメ~紙とペンと言葉から始める、キャリアの描き方~
progresscicada
2
160
今さら聞けない .NET CLI
htkym
0
200
源内ハンズオン概要編
hideg
0
190
AWS DevOps AgentのAzure接続機能を検証して見えた活用法/Use Cases Verified for the AWS DevOps Agent's Azure Connectivity Feature
masakiokuda
1
250
仕様駆動開発へのトライを機に チームに適合する手法を模索し続けている話
freee
PRO
0
580
全PRの83%がAIレビューだけでマージできるようになった開発組織はその後どうなったか
athug
1
1.9k
S3 を使うアプリケーションをローカル完結で動かすことに全力を注いでみた / Running S3 Apps Offline
contour_gara
0
540
in-process GraphQL のすすめ #ginzajs
izumin5210
4
1.4k
複数の Claude Code が"放置"されてしまう問題をCLI ダッシュボードを自作して解決した話
sumihiro3
1
710
Webエンジニアなのにブラウザの仕組みがわからないので、Pythonで自作してみた
tatsuki12
0
120
Featured
See All Featured
Chasing Engaging Ingredients in Design
codingconduct
0
280
HTML-Aware ERB: The Path to Reactive Rendering @ RubyCon 2026, Rimini, Italy
marcoroth
3
450
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
PRO
201
75k
No one is an island. Learnings from fostering a developers community.
thoeni
21
3.8k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
162
16k
Sam Torres - BigQuery for SEOs
techseoconnect
PRO
0
500
We Are The Robots
honzajavorek
0
300
SEOcharity - Dark patterns in SEO and UX: How to avoid them and build a more ethical web
sarafernandez
0
250
End of SEO as We Know It (SMX Advanced Version)
ipullrank
3
4.4k
HU Berlin: Industrial-Strength Natural Language Processing with spaCy and Prodigy
inesmontani
PRO
0
650
Abbi's Birthday
coloredviolet
3
9.4k
Data-driven link building: lessons from a $708K investment (BrightonSEO talk)
szymonslowik
1
1.2k
Transcript
API DEVELOPMENT WITH LARAVEL @SLXLIV3
TJ MILLER (YEAH, I KNOW. NOT THE ACTOR) @SIXLIV3 TJMILLER.ME
@SLXLIV3
GOALS @SLXLIV3
PLANNING @SLXLIV3
FORMATTING @SLXLIV3
{ "data": [{ "type": "articles", "id": "1", "attributes": { "title":
"JSON API paints my bikeshed!", "body": "The shortest article. Ever.", "created": "2015-05-22T14:56:29.000Z", "updated": "2015-05-22T14:56:28.000Z" }, "relationships": { "author": { "data": {"id": "42", "type": "people"} } } }], "included": [ { "type": "people", "id": "42", "attributes": { "name": "John", "age": 80, "gender": "male" } } ] } @SLXLIV3
{ "class": [ "order" ], "properties": { "orderNumber": 42, "itemCount":
3, "status": "pending" }, "entities": [ { "class": [ "info", "customer" ], "rel": [ "http: //x.io/rels/customer" ], "properties": { "customerId": "pj123", "name": "Peter Joseph" }, "links": [ { "rel": [ "self" ], "href": "http: //api.x.io/customers/pj123" } ] } ], "actions": [ { "name": "add-item", "title": "Add Item", "method": "POST", "href": "http: //api.x.io/orders/42/items", "type": "application/x- www-form-urlencoded", "fields": [ { "name": "orderNumber", "type": "hidden", "value": "42" }, ] } ], "links": [ { "rel": [ "self" ], "href": "http: //api.x.io/orders/42" }, ] } @SLXLIV3
{ "status" : "success", "data" : { "posts" : [
{ "id" : 1, "title" : "A blog post", "body" : "Some useful content" }, { "id" : 2, "title" : "Another blog post", "body" : "More content" } ] } } @SLXLIV3
VERSIONING @SLXLIV3
VERSIONING @SLXLIV3
EVOLUTION @SLXLIV3
DOCUMENTATION @SLXLIV3
OPENAPI @SLXLIV3
openapi: 3.0.0 info: version: '2018-07-01' title: Laracon Blog license: name:
MIT servers: - url: 'https: //api.sixlabs.io' paths: /posts: get: summary: List all posts operationId: post.index tags: - Posts parameters: - in: query name: published schema: type: boolean responses: '200': description: A list of posts content: application/json: schema: $ref: 'schemas/post.index.json' default: description: unexpected error content: application/json: schema: $ref: 'schemas/error.json' @SLXLIV3
openapi: 3.0.0 info: version: '2018-07-01' title: Laracon Blog license: name:
MIT servers: - url: 'https: //api.sixlabs.io' paths: /posts: get: summary: List all posts operationId: post.index tags: - Posts parameters: - in: query name: published schema: type: boolean responses: '200': description: A list of posts content: application/json: schema: $ref: 'schemas/post.index.json' default: description: unexpected error content: application/json: schema: $ref: 'schemas/error.json' @SLXLIV3
openapi: 3.0.0 info: version: '2018-07-01' title: Laracon Blog license: name:
MIT components: securitySchemes: bearerAuth: type: http scheme: bearer security: - bearerAuth: [] servers: - url: 'https: //api.sixlabs.io' @SLXLIV3
openapi: 3.0.0 info: version: '2018-07-01' title: Laracon Blog license: name:
MIT servers: - url: 'https: //api.sixlabs.io' paths: /posts: get: summary: List all posts operationId: post.index tags: - Posts parameters: - in: query name: published schema: type: boolean responses: '200': description: A list of posts content: application/json: schema: $ref: 'schemas/post.index.json' default: description: unexpected error content: application/json: schema: $ref: 'schemas/error.json' @SLXLIV3
paths: /posts: get: summary: List all posts operationId: post.index tags:
- Posts parameters: - in: query name: published schema: type: boolean responses: '200': description: A list of posts content: application/json: schema: $ref: 'schemas/post.index.json' default: description: unexpected error content: application/json: schema: $ref: 'schemas/error.json' @SLXLIV3
openapi: 3.0.0 info: version: '2018-07-01' title: Laracon Blog license: name:
MIT servers: - url: 'https: //api.sixlabs.io' paths: /posts: get: summary: List all posts operationId: post.index tags: - Posts parameters: - in: query name: published schema: type: boolean responses: '200': description: A list of posts content: application/json: schema: $ref: 'schemas/post.index.json' default: description: unexpected error content: application/json: schema: $ref: 'schemas/error.json' @SLXLIV3
paths: /posts: get: summary: List all posts operationId: post.index tags:
- Posts parameters: - in: query name: published schema: type: boolean @SLXLIV3
paths: '/posts/{id}': get: summary: Details for a specific post operationId:
post.show tags: - Posts parameters: - name: id in: path required: true description: The id of the post to retrieve schema: type: string @SLXLIV3
openapi: 3.0.0 info: version: '2018-07-01' title: Laracon Blog license: name:
MIT servers: - url: 'https: //api.sixlabs.io' paths: /posts: get: summary: List all posts operationId: post.index tags: - Posts parameters: - in: query name: published schema: type: boolean responses: '200': description: A list of posts content: application/json: schema: $ref: 'schemas/post.index.json' default: description: unexpected error content: application/json: schema: $ref: 'schemas/error.json' @SLXLIV3
responses: '200': description: A list of posts content: application/json: schema:
$ref: 'schemas/post.index.json' default: description: unexpected error content: application/json: schema: $ref: 'schemas/error.json' @SLXLIV3
/posts: post: summary: Create posts operationId: post.create tags: - Posts
requestBody: required: true content: application/json: schema: $ref: 'schemas/post.create.json' responses: '201': description: Null response @SLXLIV3
JSON SCHEMA @SLXLIV3
responses: '200': description: A list of posts content: application/json: schema:
$ref: 'schemas/post.index.json' default: description: unexpected error content: application/json: schema: $ref: 'schemas/error.json' @SLXLIV3
schemas ├── error.json ├── post.create.json ├── post.index.json ├── post.json ├──
post.show.json ├── user.index.json ├── user.json └── user.show.json @SLXLIV3
{ "data": [ { "id": 1, "title": "Nova Introduction", "post_body":
"Zurfs Up !", "user_id": 1 }, { "id": 2, "title": "Nova Part Two", "post_body": null, "user_id": 1 } ] } @SLXLIV3
{ "properties": { "id": { "format": "int64", "type": "integer" },
"post_body": { "type": [ "string", "null" ] }, "title": { "type": [ "string", "null" ] }, "user_id": { "format": "int64", "type": "integer" } }, "required": [ "id", "user_id", "title", "post_body" ], "type": "object" } @SLXLIV3
{ "properties": { "id": { "format": "int64", "type": "integer" },
"post_body": { "type": [ "string", "null" ] } }, "required": [ "id", "post_body" ], "type": "object" } @SLXLIV3
{ "properties": { "data": { "items": { "$ref": "post.json" },
"type": "array" } }, "type": "object" } @SLXLIV3
├── app ├── artisan ├── bootstrap ├── composer.json ├── config
├── database ├── openapi.yml ├── package.json ├── phpunit.xml ├── public ├── resources ├── routes ├── schemas ├── server.php ├── storage ├── tags ├── tests ├── vendor └── webpack.mix.js @SLXLIV3
DOCUMENTATION GENERATION @SLXLIV3
@SLXLIV3
IMPLEMENTATION @SLXLIV3
LARAVEL VS LUMEN @SLXLIV3
SETUP @SLXLIV3
MIDDLEWARE @SLXLIV3
protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies ::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse
::class, \Illuminate\Session\Middleware\StartSession ::class, // \Illuminate\Session\Middleware\AuthenticateSession ::class, \Illuminate\View\Middleware\ShareErrorsFromSession ::class, \App\Http\Middleware\VerifyCsrfToken ::class, \Illuminate\Routing\Middleware\SubstituteBindings ::class, ], 'api' => [ 'throttle:60,1', 'bindings', ], ]; @SLXLIV3
REMOVE MIDDLEWARE FILES └── Middleware ├── CheckForMaintenanceMode.php ├── EncryptCookies.php ├──
RedirectIfAuthenticated.php ├── TrimStrings.php ├── TrustProxies.php └── VerifyCsrfToken.php @SLXLIV3
app/Http/Kernel.php [ 'providers' => [ Illuminate\Cookie\CookieServiceProvider ::class, Illuminate\Session\SessionServiceProvider ::class, ],
'aliases' => [ 'Blade' => Illuminate\Support\Facades\Blade ::class, ], ] @SLXLIV3
ROUTE SERVICE PROVIDER app/Providers/RouteServiceProvider.php @SLXLIV3
public function map() { $this ->mapApiRoutes(); $this ->mapWebRoutes(); // }
@SLXLIV3
protected function mapApiRoutes() { Route ::prefix('api') ->middleware('api') ->namespace($this ->namespace) ->group(base_path('routes/api.php'));
} @SLXLIV3
protected function mapApiRoutes() { Route ::middleware('api') ->namespace($this ->namespace) ->group(base_path('routes/api.php')); }
@SLXLIV3
REMOVE WEB ROUTES FILE ├── routes │ ├── api.php │
├── channels.php │ ├── console.php │ └── web.php @SLXLIV3
public function map() { $this ->mapApiRoutes(); Route ::middleware('throttle:60,1') ->namespace($this ->namespace)
->group(function () { Route ::get('status', 'SystemStatusController'); }); } @SLXLIV3
app/Http/Controllers/SystemStatusController.php public function __invoke() { return response() ->json([ 'online' =>
true, 'version' => config('app.version'), ]); } @SLXLIV3
GET api.sixlabs.io/status { "online": true, "version": "2018-07-14" } @SLXLIV3
TESTING @SLXLIV3
├── tests │ ├── CreatesApplication.php │ ├── Feature │ │
└── UserApiTest.php │ └── TestCase.php @SLXLIV3
/** @test */ function fetch_a_user_index() { $user = factory(User ::class,
2) ->create(); } @SLXLIV3
/** @test */ function fetch_a_user_index() { $user = factory(User ::class,
2) ->create(); $this ->get(route('users.index')) ->assertOk(); } @SLXLIV3
Route ::get('users', 'UserController@index') ->name('users.index'); @SLXLIV3
class UserController extends Controller { public function index() { return
response(null, 200); } } @SLXLIV3
/** @test */ function fetch_a_user_index() { $user = factory(User ::class)
->create(); $this ->get(route('users.index')) ->assertOk() ->assertJsonFragment([ 'id' => $user ->id, 'name' => $user ->name, 'email' => $user ->email, ]); } @SLXLIV3
RESOURCE RESPONSES @SLXLIV3
=> App\User {#2898 name: "David Hemphill", email: "
[email protected]
", api_token: "edslIo7q1CW6",
updated_at: "2018-07-21 01:17:26", created_at: "2018-07-21 01:17:26", id: 1, } @SLXLIV3
class UserController extends Controller { public function index() { return
response() ->json([ 'data' => User ::all() ->toArray() ]); } } @SLXLIV3
{ "data": [{ "id": 1, "email": "
[email protected]
", "name": "David Hemphill",
"api_token": "edslIo7q1CW6", "created_at": "2018-07-21 01:17:26", "updated_at": "2018-07-21 01:17:26" }] } @SLXLIV3
class UserController extends Controller { public function index() { $users
= User ::select([ 'id', 'name', 'created_at', ]) ->get(); return response() ->json([ 'data' => $users, ]); } } @SLXLIV3
class UserController extends Controller { public function index() { $users
= User ::select([ 'id', 'name', 'created_at', ]) ->get(); return response() ->json([ 'data' => $users, ]); } public function show($id) { $user = User ::select([ 'id', 'name', 'created_at', ]) ->findOrFail($id); return response() ->json([ 'data' => $user, ]); } } @SLXLIV3
class PostController extends Controller { public function show($id) { $post
= Post ::with('author') ->findOrFail($id); // Formatting for Post AND User return response() ->json(['data' => $post]); } } @SLXLIV3
class UserResource extends JsonResource { public function toArray($request) { return
[ 'id' => $this ->id, 'email' => $this ->email, 'name' => $this ->name, 'created_at' => $this ->created_at, ]; } } @SLXLIV3
use App\Http\Resources\UserResource; class UserController extends Controller { public function index()
{ return UserResource ::collection(User ::all()); } public function show($id) { return UserResource ::make(User ::findOrFail($id)); } } @SLXLIV3
class PostResource extends JsonResource { public function toArray($request) { return
[ 'id' => $this ->id, 'title' => $this ->title, 'post_body' => $this ->post_body, 'author' => UserResource ::make($this ->whenLoaded('author')), ]; } } @SLXLIV3
class PostResource extends JsonResource { public function toArray($request) { return
[ 'id' => $this ->id, 'title' => $this ->title, 'post_body' => $this ->post_body, 'author' => UserResource ::make($this ->whenLoaded('author')), ]; } } @SLXLIV3
class PostController extends Controller { public function show($id) { return
PostResource ::make( Post ::with('author') ->findOrFail($id) ); } } @SLXLIV3
class UserResource extends JsonResource { public function toArray($request) { return
[ 'id' => $this ->id, 'email' => $this ->email, 'name' => $this ->name, 'api_token' => $this ->when( $this ->canViewApiToken(), $this ->api_token ), 'created_at' => $this ->created_at, ]; } private function canViewApiToken() { return auth() ->user() ->can('user.api_token.view', $this); } } @SLXLIV3
class UserResource extends JsonResource { public function toArray($request) { return
[ 'id' => $this ->id, 'email' => $this ->email, 'name' => $this ->name, 'api_token' => $this ->when( $this ->canViewApiToken(), $this ->access_token ), 'created_at' => $this ->created_at, ]; } } @SLXLIV3
class PostController extends Controller { public function index() { return
PostResource ::collection(Post ::all() ->paginate()); } } @SLXLIV3
{ "data": [ { "id": 1, "post_body": "Zurfs Up !",
"title": "Nova Introduction", "user_id": 1 }, { "id": 2, "post_body": null, "title": "Nova Part Two", "user_id": 1 } ], "links": { "first": "https: //api.sixlabs.io/posts?page=1", "last": "https: //api.sixlabs.io/posts?page=1", "next": null, "prev": null }, "meta": { "current_page": 1, "from": 1, "last_page": 1, "path": "https: //api.sixlabs.io/posts", "per_page": 15, "to": 1, "total": 1 } } @SLXLIV3
/** @test */ function fetch_a_user_index() { $user = factory(User ::class)
->create(); $this ->get(route('users.index')) ->assertOk() ->assertJsonFragment([ 'id' => $user ->id, 'name' => $user ->name, 'email' => $user ->email, ]) } @SLXLIV3
class UserResource extends JsonResource { public function toArray($request) { return
[ 'id' => $this ->id, 'email' => $this ->email, 'name' => $this ->name, 'created_at' => $this ->created_at, ]; } } @SLXLIV3
use App\Http\Resources\UserResource; class UserController extends Controller { public function index()
{ return UserResource ::collection(User ::all()); } } @SLXLIV3
/** @test */ function fetch_a_user_index() { $user = factory(User ::class)
->create(); $this ->get(route('users.index')) ->assertOk() ->assertJsonFragment([ 'id' => $user ->id, 'name' => $user ->name, 'email' => $user ->email, ]) } @SLXLIV3
/** @test */ function fetch_a_user_index() { $user = factory(User ::class)
->create(); $this ->get(route('users.index')) ->assertOk() ->assertJsonFragment([ 'id' => $user ->id, 'name' => $user ->name, 'email' => $user ->email, ]) ->assertJsonSchema('user.index'); } @SLXLIV3
sixlive/laravel-json-schema-assertions @SLXLIV3
AUTHENTICATION @SLXLIV3
GET /posts HTTP/2.0 Authorization: Bearer t82BJLrM3jONOXd7YnoKgSFNTlXuaQFZLTmmMsIIodsktiuVdMlmlFQGJtkduZhD Content-Type: application/json Host: api.sixlive.io
@SLXLIV3
class CreateUsersTable extends Migration { public function up() { Schema
::create('users', function (Blueprint $table) { $table ->increments('id'); $table ->string('name'); $table ->string('email') ->unique(); $table ->string('password'); $table ->string('api_token', 64); $table ->rememberToken(); $table ->timestamps(); }); } } @SLXLIV3
public static function boot() { parent ::boot(); static ::creating(function ($user)
{ $user ->api_token = str_random(64); }); } @SLXLIV3
'guards' => [ 'web' => [ 'driver' => 'session', 'provider'
=> 'users', ], 'api' => [ 'driver' => 'token', 'provider' => 'users', ], ], @SLXLIV3
'guards' => [ 'web' => [ 'driver' => 'token', 'provider'
=> 'users', ], 'api' => [ 'driver' => 'token', 'provider' => 'users', ], ], @SLXLIV3
Route ::get('posts', 'PostController@index') ->name('posts.index'); Route ::get('posts/{id}', 'PostController@show') ->name('posts.show'); Route ::middleware('auth')
->group(function () { Route ::post('posts', 'PostController@store') ->name('posts.store'); }); @SLXLIV3
/** @test */ function fields_are_required_to_create_a_post() { $user = factory(User ::class)
->create(); $this ->actingAs($user); $response = $this ->json('POST', route('posts.store'), []); $response ->assertStatus(Response ::HTTP_UNPROCESSABLE_ENTITY) ->assertJsonValidationErrors([ 'title', 'post_body' ]); } @SLXLIV3
public function store() { request() ->validate([ 'title' => 'required', 'post_body'
=> 'required', ]); } @SLXLIV3
/** @test */ public function a_user_can_create_a_post() { $user = factory(User
::class) ->create(); $this ->actingAs($user); $response = $this ->json('POST', route('posts.store'), [ 'title' => 'Laracon Spotlight: Wes Bos', 'post_body' => 'Wicked!' ]); $response ->assertStatus(Response ::HTTP_CREATED) ->assertJsonFragment([ 'post_body' => 'Wicked!', 'title' => 'Laracon Spotlight: Wes Bos', 'user_id' => $user ->id, ]) ->assertJsonSchema('post.show'); } @SLXLIV3
public function store() { request() ->validate([ 'title' => 'required', 'post_body'
=> 'required', ]); $post = request() ->user() ->posts() ->create( request() ->only(['title', 'post_body']) ); return PostResource ::make($post); } @SLXLIV3
spatie/laravel-query-builder @SLXLIV3
https: //localhost/posts?filter[published]=true /** @test */ public function can_filter_by_published() { $user
= factory(User ::class) ->create(); $posts = factory(Post ::class, 2) ->create([ 'user_id' => $user ->id, 'published' => true, ]); $posts[1] ->update(['published' => false]); $route = route('posts.index', [ 'filter' => [ 'published' => true ] ]); $this ->get($route) ->assertJsonCount(1, 'data') ->assertJsonFragment([ 'title' => $posts[0] ->title, 'post_body' => $posts[0] ->post_body, ]) ->assertJsonSchema('post.index'); } @SLXLIV3
https: //localhost/posts?filter[published]=true /** @test */ public function can_filter_by_published() { $user
= factory(User ::class) ->create(); $posts = factory(Post ::class, 2) ->create([ 'user_id' => $user ->id, 'published' => true, ]); $posts[1] ->update(['published' => false]); $route = route('posts.index', [ 'filter' => [ 'published' => true ] ]); $this ->get($route) ->assertJsonCount(1, 'data') ->assertJsonFragment([ 'title' => $posts[0] ->title, 'post_body' => $posts[0] ->post_body, ]) ->assertJsonSchema('post.index'); } @SLXLIV3
https: //localhost/posts?filter[published]=true /** @test */ public function can_filter_by_published() { $user
= factory(User ::class) ->create(); $posts = factory(Post ::class, 2) ->create([ 'user_id' => $user ->id, 'published' => true, ]); $posts[1] ->update(['published' => false]); $route = route('posts.index', [ 'filter' => [ 'published' => true ] ]); $this ->get($route) ->assertJsonCount(1, 'data') ->assertJsonFragment([ 'title' => $posts[0] ->title, 'post_body' => $posts[0] ->post_body, ]) ->assertJsonSchema('post.index'); } @SLXLIV3
https: //localhost/posts?filter[published]=true /** @test */ public function can_filter_by_published() { $user
= factory(User ::class) ->create(); $posts = factory(Post ::class, 2) ->create([ 'user_id' => $user ->id, 'published' => true, ]); $posts[1] ->update(['published' => false]); $route = route('posts.index', [ 'filter' => [ 'published' => true ] ]); $this ->get($route) ->assertJsonCount(1, 'data') ->assertJsonFragment([ 'title' => $posts[0] ->title, 'post_body' => $posts[0] ->post_body, ]) ->assertJsonSchema('post.index'); } @SLXLIV3
class PostController extends Controller { public function index() { $posts
= Post ::all(); return PostResource ::collection($posts); } } @SLXLIV3
use Spatie\QueryBuilder\QueryBuilder; class PostController extends Controller { public function index()
{ $posts = QueryBuilder ::for(Post ::class) ->allowedFilters('published') ->get(); return PostResource ::collection($posts); } } @SLXLIV3
RECOMMENDED PACKAGES ▸ Laravel CSP (spatie/laravel-csp) ▸ Laravel CORS (spatie/laravel-cors)
▸ Laravel Query Builder (spatie/laravel-query-builder) ▸ Bouncer (silber/bouncer) ▸ JSON Schema Assertions (sixlive/laravel-json-schema-assertions) @SLXLIV3
@SLXLIV3
APISWITHLARAVEL.COM @SLXLIV3
THANK YOU @SLXLIV3