<!DOCTYPE html>
<html>

  <head>
    <script data-require="angular.js@1.3.15" data-semver="1.3.15" src="https://code.angularjs.org/1.3.15/angular.js"></script>
    <script src="https://code.angularjs.org/1.3.15/angular-messages.js"></script>
    <script src="https://code.angularjs.org/1.3.15/angular-animate.js"></script>
    <script src="https://code.angularjs.org/1.3.15/angular-aria.js"></script>
    
    <link data-require="material_design@0.9.0" data-semver="0.9.0" rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/angular_material/0.9.0/angular-material.min.css" />
    <link data-require="material_design@0.9.0" data-semver="0.9.0" rel="stylesheet" href="https://fonts.googleapis.com/css?family=RobotoDraft:300,400,500,700,400italic" />
    <script data-require="material_design@0.9.0" data-semver="0.9.0" src="https://ajax.googleapis.com/ajax/libs/angular_material/0.9.0/angular-material.min.js"></script>
    
    <script src="sample-app.js"></script>
    <script src="api-form-validation-service.js"></script>
    <script src="api-validate-directive.js"></script>
    <script src="api-form-directive.js"></script>
    <script src="api-service.js"></script>
    <script src="sample-form-controller.js"></script>
    <script src="sample-form-directive.js"></script>
  </head>

  <body ng-app="sampleApp">
    <sample-form></sample-form>
  </body>

</html>
(function(){
  'use strict';
  
  angular.module('sampleApp', [
    'ngMessages',
    'ngMaterial'
  ]);
  
})();
No error handling in view controller. Use directive to handle API errors that:

1. Shows general api errors in given place in form (e.g. top of the form). This is when something happens and we don’t really know what (server failed)
2. Shows field related error next to the field as I would show standard validation error.
3. Shows some errors in general area that are field related but that field is not found in form.
4. Let's you decide if particular form field should have error attached to that field or should it fall into “general error” area.

And all of that using ng-messages, so no additional custom components needed.
(function(){
  'use strict';
  
  angular.module('sampleApp')
    .directive('sampleForm', sampleForm);
  
  function sampleForm () {
    return {
      restrict: 'E',
      templateUrl: 'sample-form.html',
      scope: {},
      controller: 'SampleFormController',
      controllerAs: 'ctrl',
      bindToController: true
    };
  }
  
})();
(function(){
  'use strict';
  
  angular.module('sampleApp')
    .controller('SampleFormController', SampleFormController);
  
  
  function SampleFormController (apiService) {

    // expose denendencies on this
    this.api = apiService;

    // expose data models
    this.formModel = {};
    this.savePromise = null;
  }


  SampleFormController.prototype.save = function (form) {

    // do not send when form is invalid
    if (form.$invalid) {
      return;
    }

    return (this.savePromise = this.api.post(this.formModel));
  }

  SampleFormController.$inject = ['apiService'];
  
})();
(function(){
  'use strict';
  
  angular.module('sampleApp')
    .factory('ApiFormValidationService', ApiFormValidationService);
  
  function ApiFormValidationService() {
    return {
      applyErrors: applyErrors,
      applyError: applyError,
      reset: reset,
      setInvalid: setInvalid,
      setValid: setValid
    };
  
  
  
    function applyErrors (form, reason) {
      var data = reason.data;
  
      if (!form.$api) {
        reset(form);
      }
  
      // no data returned, fallback to system error
      if (!data) {
        form.$api.$error['systemError'] = true;
        setInvalid(form);
        return;
      }
      else {
  
        // this is general error not connected to any field
        if (data.code) {
          form.$api.$error[data.code] = true;
          setInvalid(form);
        }
  
        angular.forEach(data.fieldErrors, applyError.bind(null, form));
      }
    }
  
  
    function applyError (form, error) {
      var code = error.code,
        field = error.field,
        api;
        
      if (!form.$api) {
        reset(form);
      }
      
      api = form.$api;
  
      // field does is not able to handle API errors
      if (!form[field] || !form[field].$registerApiError) {
        if (!api.$error[code]) {
          api.$error[code] = [];
        }
  
        api.$error[code].push(field);
        setInvalid(form);
      }
      // field exists and is able to handle API errors
      else {
        form[field].$registerApiError(code);
      }
    }
  
  
    function reset (form){
      form.$api = {
        $error: {},
        $invalid: false,
        $valid: true
      };
    }
  
  
    function setInvalid (form){
      if (!form.$api){
        reset(form);
      }
      
      form.$api.$invalid = true;
      form.$api.$valid = false;
    }
  
  
    function setValid (form){
      if (!form.$api){
        reset(form);
      }
      
      form.$api.$invalid = false;
      form.$api.$valid = true;
    }
  }
  
})();
(function(){
  'use strict';
  
  angular.module('sampleApp')
    .directive('apiValidate', apiValidate);
  
  function apiValidate () {
    
    // storage for current API errors, to be removed once model changed
    var errors = [],
      model;
    
    return {
      require: 'ngModel',
      restrict: 'A',
      link: postLink
    };



    function postLink(scope, element, attrs, ngModel) {
      model = ngModel;

      // expose interface to register API error for this field
      model.$registerApiError = registerApiError;

      // add validator to validation loop, that will remove current API errors
      model.$validators.apiValidate = apiValidator;
    }


    function apiValidator () {

      // validation loop, remove all field api errors
      while (errors.length) {
        model.$setValidity(errors.shift(), true);
      }

      // this is fake validator, just return true
      return true;
    }


    function registerApiError (code){
      errors.push(code);

      model.$setValidity(code, false);
    }
  }
  
})();
(function(){
  'use strict';
  
  angular.module('sampleApp')
    .directive('apiForm', apiForm);
  
  function apiForm (ApiFormValidationService) {
    return {
      require: 'form',
      restrict: 'A',
      link: postLink
    };



    function postLink (scope, element, attrs, formCtrl) {
      var watchPromise = attrs.apiForm || null;

      if(watchPromise !== void 0){
        scope.$watch(watchPromise, formSubmitted.bind(null, formCtrl));
      }
    }


    function formSubmitted (form, promise) {
      if(!promise || !promise.then){
        return;
      }
      
      ApiFormValidationService.reset(form);

      promise.then(
        null,
        function failure (reason){
          ApiFormValidationService.applyErrors(form, reason);
        }
      );
    }
  }

  apiForm.$inject = ['ApiFormValidationService'];
  
})();
<!-- could use <div ng-controller="MyApiFormController as ctrl"> in traditional mode -->

<md-content layout-padding>
  <form name="myForm" api-form="ctrl.savePromise">
    <div ng-if="myForm.$api.$invalid" style="margin-bottom:15px;">
      <ng-messages for="myForm.$api.$error" role="alert" style="color: rgb(244,67,54);">
        <ng-message when="global">API tells that there is some global error.</ng-message>
        <ng-message when="required">API tells that it requires field: {{myForm.$api.$error['required'].join(',')}}</ng-message>
      </ng-messages>
      <br>
    </div>
  
    <md-input-container flex>
      <label>Some input with API errors</label>
      <input type="text" name="fieldName" ng-model="ctrl.formModel.fieldModel" api-validate/>
      <ng-messages for="myForm.fieldName.$error" role="alert">
        <ng-message when="required">API tells that this is required.</ng-message>
        <ng-message when="nonsense">API tells that this is really some nonsense.</ng-message>
      </ng-messages>
    </md-input-container>
    
    <md-input-container flex>
      <label>Some input without API errors</label>
      <input type="text" name="fieldName2" ng-model="ctrl.formModel.fieldModel2"/>
    </md-input-container>
    
    <section layout="row" layout-sm="column" layout-align="center center">
      <md-button class="md-raised" type="submit" ng-click="ctrl.save(myForm)" ng-disabled="myForm.$invalid">Save</md-button>
    </section>
  </form>
</md-content>
<md-content layout-padding>
  <p>
    Just click Save couple of times (remember to modify input when field error is there).
    Errors are a bit random :)
  </p>
</md-content>
(function(){
  'use strict';
  
  angular.module('sampleApp')
    .factory('apiService', apiService);
  
  function apiService ($q, empty) {
    return {
      post: post
    };
    
    
    
    function post (data) {
      return $q.reject({
        data: getFakeError(!data.fieldModel || !data.fieldModel.length)
      });
    }
    
    
    function getFakeError (empty) {
      var fakeErrors = [
        {
          code: 'global',
          fieldErrors: (!empty ? [] : [
            {
              field: 'fieldName',
              code: 'required'
            }
          ])
        },
        {
          fieldErrors: [
            {
              field: 'fieldName',
              code: empty ? 'required':'nonsense'
            }
          ]
        },
        {
          code: 'global',
          fieldErrors: [
            {
              field: 'fieldName',
              code: empty ? 'required':'nonsense'
            }
          ]
        },
        {
          fieldErrors: [
            {
              field: 'fieldName',
              code: empty ? 'required':'nonsense'
            },
            {
              field: 'notExistingField',
              code: 'required'
            }
          ]
        }
      ];
      
      return fakeErrors[Math.floor(Math.random() * 4)];
    }
  }
  
  apiService.$inject = ['$q'];
  
})();