Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
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
Simple Storage Service(S3) is not simple
iwatsukayura
0
290
カツオ、ご期待ください
suneo3476
0
190
當我們開始不讀 Code - AI Agent 時代,Code Review 與 Clean Code 還剩下什麼?
jame2408
1
210
Rでドレミ/Do-Re-Mi_with_R
florets1
0
130
Findy - エンジニア向け会社紹介/Findy Company Deck
findyinc
6
400k
月間100万人のサービス刷新で、 技術選定をHonoに振り切った話
yutobara
8
6.6k
AI時代のコードレビューは人に向けるな、仕組みに向けろ
texmeijin
5
3.7k
3回つくってわかった、 定期実行ジョブ基盤の本質情報
takai
0
460
最近”作らせてる” ローカルAIアプリの話
akit37
0
470
RAG の “R” を Swift で覗いてみる 〜「意味から探す」検索の仕組み〜
nao_randd
0
160
パスワード再設定メールのURLについて知っておこう
uyuki234
0
160
Domain-Driven Transformation
hschwentner
2
2.3k
Featured
See All Featured
Organizational Design Perspectives: An Ontology of Organizational Design Elements
kimpetersen
PRO
1
850
Sam Torres - BigQuery for SEOs
techseoconnect
PRO
0
580
Site-Speed That Sticks
csswizardry
13
1.6k
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
PRO
203
77k
Exploring the relationship between traditional SERPs and Gen AI search
raygrieselhuber
PRO
4
4.4k
[RailsConf 2023] Rails as a piece of cake
palkan
59
7.1k
Pawsitive SEO: Lessons from My Dog (and Many Mistakes) on Thriving as a Consultant in the Age of AI
davidcarrasco
0
260
ラッコキーワード サービス紹介資料
rakko
1
5.3M
Bootstrapping a Software Product
garrettdimon
PRO
306
120k
Bash Introduction
62gerente
615
220k
Designing for Performance
lara
612
70k
Learning to Love Humans: Emotional Interface Design
aarron
275
41k
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