Micro-frameworks Make Awesome APIs


Austin S. Morris
@austinsmorris



Slides available at https://austinsmorris.github.io/micro-frameworks-make-awesome-apis

About Me

I PHP:

First dabbled in college (early-2000s).

Personal projects while masquerading as an aerospace engineer.

Full stack development for last several years.

For Money:

Senior Software Engineer

Varsity News Network

(We do the things in this presentation!)

For Free:

Co-Author of Peridot

The highly extensible, highly enjoyable, BDD testing framework for PHP.

(It's how I test the things in this presentation!)

I want an API!

I want an awesome API!

What makes an awesome API?

An awesome API is...

Domain Focused!

My domain is my business.

My business is my domain.

An API supports the domain!

How data is accessed.

Actions taken on data.

Business rules are applied to actions and data.

So how should others

interact with my API?

REST is cool!

But, what is it?

Richardson Maturity Model

A measurement of your API's RESTfulness.

Think of this from the client prospective.

RMM Level 0

Swamp of POX

POST or GET everything to/from a single api endpoint.
The request content determines the action.
(Remote Procedure Invocation)

RMM Level 1

Resources

Divide and conquer complexity with different resource endpoints.
The request content still determines the action.

RMM Level 2

Protocol Standardization

Use the protocol (HTTP verbs) to determine resource actions.
GET a resource or collection.
DELETE a resource.
PATCH a resource.
POST to a collection.

RMM Level 3

Hypermedia As The Engine Of Application State

Use links to declare actions and resources.
Allows discoverability and automation.

Requirement #1!

RMM Level 1 Resources

+

RMM Level 2 Verbs

=

We need a router!

What about RMM Level 3?

This is part of your API design.

(Not a requirement of your framework)

An awesome API is...

Well Tested!

Prefer unit testing.

(fast, stable, reliable)

Resort to integration/functional testing.

(slower and more fragile)

Code is easily tested when..

  • It is isolated and singular in purpose.
  • External dependencies are injected.
  • Each piece has a simple public api.

An awesome API is...

Maintainable!

Testability and maintainablity

go hand in hand.

Use good design principles.

Loosely-coupled objects that depend on abstractions.

Small highly-cohesive objects with a single responsibility.

DRY, KISS, SOLID, etc...

Requirement #2!

Something has to inject those dependencies...

We need an IOC layer!

(Let's just call it the container.)

The container will..

  • Give us access to our domain services.
  • Resolve our dependency abstractions.
  • Glue layers of our application together.

An awesome API must...

Respond to a Request!

(And handle everything in between.)

Authenticate

Authorize

Negotiate Content

De-serialize

Hydrate

Validate

Perform an action!

Serialize

Do even more things (post-request)

How???

Requirement #3!

We need middlewares!

Middleware is software that connects

other software components.


For an API, it connects a request to a response.

Middlewares can be placed:

  • Before or after routing.

  • Before or after route handling (the action).

  • Before or after the response
    fastcgi_finish_request();

Middlewares give us many

small, reusable pieces.

  • Better testability.

  • Better OOP.

  • Better domain access.

Putting it all together...

Micro-Frameworks!

A micro-framework glues together:

  • Router

  • Container

  • Middlewares

Into a working application

that is focused on the domain.

It's everything your API needs!

(without the stuff it doesn't)

What about full-stack frameworks?

They are the stuff you don't need.

All that stuff can be slower.

Start with the simplest thing that works (be agile).

Because awesome APIs are already hard enough!

I haz teh codez!

Micro-frameworks 101

Hello Silex!


use Silex\Application;
use Symfony\Component\HttpFoundation\Response;

$app = new Application();

$app->get('/hello/world', function () {
  return new Response('Hello World!');
});

$app->run();
          

Hello Lumen!


use Symfony\Component\HttpFoundation\Response;

$app = require __DIR__.'/../bootstrap/app.php';

$app->get('/hello/world', function () {
  return new Response('Hello World!');
});

$app->run();
          

Non-trivial Silex!


$app['my.resource.repository'] = $app->share(function (Application $app) {
  return new My\Resource\Repository($app['my.db.connection']);
});

$app->get('my/resource/{id}', function ($app, $request, $id) {
  $token = $app['my.auth.token.repository']->find($request->get('access_token'));
  if (!$token) {
    return new Response('', Response::HTTP_UNAUTHORIZED);
  }

  $resource = $app['my.resource.repository']->find($id);
  if (!$resource) {
    return new Response('', Response::HTTP_NOT_FOUND);
  }

  return new Response(json_encode($resource->toArray()));
});
          

Non-trivial Lumen!


$app->bind(Repository::class, function  () {
  return new My\Resource\Repository($app['my.db.connection']);
});

$app->get('my/resource/{id}', function ($id) use ($app) {
  $token = $app['My\Auth\Token\Repository']->find($app['request']->get('access_token'));
  if (!$token) {
    return new Response('', Response::HTTP_UNAUTHORIZED);
  }

  $resource = $app[Repository::class]->find(123);
  if (!$resource) {
    return new Response('', Response::HTTP_NOT_FOUND);
  }

  return new Response(json_encode($resource->toArray()));
});
          

It's a trap!


Let's look again...

Non-trivial Silex!


$app['my.resource.repository'] = $app->share(function (Application $app) {
  return new My\Resource\Repository($app['my.db.connection']);
});

$app->get('my/resource/{id}', function ($app, $request, $id) {
  $token = $app['my.auth.token.repository']->find($request->get('access_token'));
  if (!$token) {
    return new Response('', Response::HTTP_UNAUTHORIZED);
  }

  $resource = $app['my.resource.repository']->find($id);
  if (!$resource) {
    return new Response('', Response::HTTP_NOT_FOUND);
  }

  return new Response(json_encode($resource->toArray()));
});
          

Non-trivial Lumen!


$app->bind(Repository::class, function  () {
  return new My\Resource\Repository($app['my.db.connection']);
});

$app->get('my/resource/{id}', function ($id) use ($app) {
  $token = $app['My\Auth\Token\Repository']->find($app['request']->get('access_token'));
  if (!$token) {
    return new Response('', Response::HTTP_UNAUTHORIZED);
  }

  $resource = $app[Repository::class]->find(123);
  if (!$resource) {
    return new Response('', Response::HTTP_NOT_FOUND);
  }

  return new Response(json_encode($resource->toArray()));
});
          

Silex Controllers


$app->register(new Silex\Provider\ServiceControllerServiceProvider());

$app['my.resource.controller'] = $app->share(function ($app) {
  return new \My\Resource\Controller($app['my.resource.repository']);
});

$app->get('my/resource/{id}', 'my.resource.controller:get');
          
namespace My\Resource;

use My\Resource\RepositoryInterface;
use Symfony\Component\HttpFoundation\Response;

class Controller
{
  protected $repo;

  public function __construct(RepositoryInterface $repo)
  {
    $this->repo = $repo;
  }

  public function get($app, $request, $id)
  {
    $resource = $this->repo->find($id);
    if (!$resource) {
      return new Response('', Response::HTTP_NOT_FOUND);
    }

    return new Response(json_encode($resource->toArray()));
  }
}
          

Lumen Controllers


use My\Resource\Repository;
use My\Resource\RepositoryInterface;

$app->bind(RepositoryInterface::class, Repository::class);

$app->get('my/resource/{id}', 'ResourceController@get');
          
namespace App\Http\Controllers;

use Laravel\Lumen\Routing\Controller as BaseController;
use My\Resource\RepositoryInterface;
use Symfony\Component\HttpFoundation\Response;

class ResourceController extends BaseController
{
  protected $repo;

  public function __construct(RepositoryInterface $repo)
  {
    $this->repo = $repo;
  }

  public function get($id)
  {
    $resource = $this->repo->find($id);
    if (!$resource) {
      return new Response('', Response::HTTP_NOT_FOUND);
    }

    return new Response(json_encode($resource->toArray()));
  }
}
          

Silex Container


use Pimple;
// ...

class Application extends Pimple implements HttpKernelInterface, TerminableInterface
{
  // ...
}
          

$app['my.parameter'] = 123;

$app['my.service'] = function () { /* This function will be invoked */ };

$app['my.shared.service'] = $app->share(function ($app) {
  return $app['my.service']  // only instantiated once!
});

$foo = $app['the.thing.i.want'];
          

Lumen Container


use Illuminate\Container\Container;
// ...

class Application extends Container implements ApplicationContract, HttpKernelInterface
{
  // ...
}
          

$app->instance('my.parameter', 123);

$app->bind('My\Service',  function () { /* This function will be invoked */ };

$app->singleton('My\Shared\Service', function ($app) {
  return $app['My\Service']  // only instantiated once!
});

$app->bind('My\Interface', 'My\Implementation');

$foo = $app['My\Thing'];
$bar = $app->make('My\Other\Thing');
          

Plus...

Auto-resolving

Contextual Bindings

Tagging

Container Events

Silex Middleware


$app->before('my.before.routing.middleware:doAThing');

$app->get('/my/resource', 'my.resource.controller:get')
  ->before('my.before.this.route.middlware:doSomething')
  ->after('my.after.this.route.middlware:doSomethingElse');

$app->after('my.after.route.after.middlewares:doMore');

$app->finish('my.after.response.is.returned.middleare:doSoemthingSlow');
          

namespace My\Middlware;

class BeforeRouteMiddlware
{
  protected $dependecy;

  public function __construct($dependency)
  {
    $this->dependency = $dependency;
  }

  public function doSomething($request, $app)
  {
    // do something..
  }
{
          

Lumen Middleware


$app->middleware([
  App\Http\Middleware\BeforeRoutingMiddleware::class,
  App\Http\Middleware\AfterRoutingMiddleware::class,
  App\Http\Middleware\TerminableMiddleware::class,
]);

$app->routeMiddleware([
  'before' => App\Http\Middleware\BeforeRouteMiddleware::class,
  'after' => App\Http\Middleware\AfterRouteMiddleware::class,
]);

$app->get('my/resource/{id}', [
  'uses' => 'ResourceController@get',
  'middleware' => ['before', 'after'],
]);
          

use Closure;

class BeforeMiddleware
{
  public function handle($request, Closure $next)
  {
    // do something here
    return $next($request);
  }
}
          

use Closure;

class AfterMiddleware
{
  public function handle($request, Closure $next)
  {
    $response = $next($request);
    // do something here
    return $response;
  }
}
          

use Closure;

class TerminableMiddleware
{
  public function handle($request, Closure $next)
  {
    return $next($request);
  }

  public function terminate($request, $response)
  {
    // do stuff after the request was sent
  }
}
          

Auth[entication|orization]

The user is who they claim to be.

The user is allowed to do this.

Lumen


$app->routeMiddleware([
  'auth' => App\Http\Middleware\ResourceAuthMiddleware::class,
]);

$app->post('my/resource', [
  'uses' => 'ResourceController@post',
  'middleware' => ['auth'],
]);
          

class ResourceAuthMiddleware
{
  protected $tokenRepo;

  public function __construct(TokenRepository $tokenRepo)
  {
    $this->tokenRepo = $tokenRepo;
  }

  public function handle($request, Closure $next)
  {
    $token = $this->tokenRepo->find($request->get('access_token'));
    if (!$token) {
      return new Response('Please login.', Response::HTTP_UNAUTHORIZED);
    }

    if (!$token->hasPermission('resource')) {
      return new Response('Go Away!', Response::HTTP_FORBIDDEN);
    }

    return $next($request);
  }
}
          

Use the right status codes!

  • 401: Unauthorized
  • 403: Forbidden


Headers are cool too!

  • Authorize

Content Negotiation

Make sure the computers are

speaking the same language.

Silex


$app['ContentNegotiation'] = $app->share(function () {
  return new ContentNegotiation();
)};
          

$app->post('/my/resource', 'ResourceController:post')
  ->before('ContentNegotiation:checkForJson');
          

class ContentNegotiation
{
  public function checkForJson(Request $request)
  {
    if (!strstr('application/json', $request->headers->get('Content-Type'))) {
        return new Response('', Response::HTTP_UNSUPPORTED_MEDIA_TYPE);
    }
  }
}
          

Use the right status codes!

  • 406: Not Acceptable
  • 415: Unsupported Media Type


Use the right headers!

  • Accept
  • Content-Type

De-serialization

Covert the raw request data (JSON)

into a native type (array|object).

Lumen


$app->routeMiddleware([
  'authenticate' => App\Http\Middleware\Authentication::class,
  'authorize' => App\Http\Middleware\ResourceAuthorization::class,
  'negotiateContent' => App\Http\Middleware\ContentNegotiation::class,
  'jsonDecode' => App\Http\Middleware\JsonDecode::class,
]);

$app->post('my/resource', [
  'uses' => 'ResourceController@post',
  'middleware' => ['authenticate', 'authorize', 'negotiateContent', 'jsonDecode'],
]);
          

class JsonDecode
{
  protected $app;

  public function __construct(Application $app)
  {
    $this->app = $app;
  }

  public function handle($request, Closure $next)
  {
    $decodedData = json_decode($request->getContent(), true);
    if ($decodedData === null) {
      return new Response('Invalid json content.', Response::HTTP_BAD_REQUEST);
    }

    $this->app->instance('request_data', $decodedData);  // optional
    $next($request, $decodedData)
  }
}
          

Use the right status code!

  • 400: Bad Request


Put the data somewhere useful.

  • In the container.
  • On the request.
  • Next middleware.

Hydration

Fetch or create the requested resource.


$app->run(My\Http\Request::createFromGlobals());
          
namespace My\Http;

use Symfony\Component\HttpFoundation\Request as HttpFoundationRequest;

class Request extends HttpFoundationRequest
{
  protected $requestArray;
  protected $requestResource;

  // include accessor methods...
}
          

Silex


class ResourceHydrator
{
  protected $resourceFactory

  public function __construct(ResourceFactory $resourceFactory)
  {
    $this->resourceFactory = $resourceFactory;
  }

  public function hydrateFromArray($request)
  {
    $resource = $this->resourceFactory->fromArray($resource->getResourceArray());
    $request->setRequestResource($resource);
  }
}
          

Use the middleware to access domain services.


Use the right status code!

  • 404: Not Found

Validation

Does this request follow my business rules?

Caution, validation will blur the lines!


Use the middleware to access domain services.


Leverage a validation library

in your domain services.

Use the right status code!

  • 400: Bad Request


Provide useful error messages.

  • application/api-problem+json

[The Action]

The thing the route does!


class MyController
{
  protected $repository

  public function __construct(RepositoryInterface $repository)
  {
    $this->repository = $repository;
  }

  public function postAction($request)
  {
    $this->repository->add($request->getResource)->flush();
    return new Response('the content', Response::HTTP_CREATED;
  }
}
          

Use the right status code!

  • 200: OK
  • 201: Created
  • 204: No Content

Serialization

Create the resource representation.

Don't re-invent the wheel.

  • application/hal+json
  • application/vnd.api+json
  • application/vnd.collection


Use hypermedia to achieve RMM Level 3.

Post-request jobs

Do slow things after request is sent.

  • Independent of the request/response life-cycle
  • Requires
    fastcgi_finish_request();

Finish/Terminable middlewares are

defined independent of any routing.


Often, we want to apply them

to specific, individual routes.

Silex


$app->post('/my/resource', 'ResourceController:post')
  ->after('FinishMiddlewareCreator:registerFinishMiddleware');
          

class FinishMiddlewareCreator
{
  public function registerFinishMiddleware($request, $response, $app)
  {
    $app->finish('TheRealFinishMiddleware:doSomethingSlow');
  }
}
          

Lumen


$app->routeMiddleware(['after' => MyAfterRouteMiddleware::class]);
$app->post('my/resource/{id}', [
  'uses' => 'ResourceController@get',
  'middleware' => ['after'],
]);
          

class MyAfterRouteMiddleware
{
  protected $app

  public function __construct($app)
  {
    $this->app = $app;
  }

  public function handle($request, Closure $next)
  {
    $response = $next($request);
    $this->app->middleware([MyTerminableMiddleware::class]);
    return $response;
  }
}
          

tl;dr

A micro-framework is a simple collection of

a router, a container, and middlewares.


It is the ideal platform for a fast, reliable

testable, maintainable, and RESTful API.

The middlewares allow the isolation

of each piece of API responsibility.


They break API behaviors down into

interchangeable, reusable pieces of code.

Questions?