diff --git a/composer.json b/composer.json
index b43f69f5d277f9d092f7abfa57d3b45a25ed338b..7606ec7e58202fcf997212c892fc959bf775011d 100644
--- a/composer.json
+++ b/composer.json
@@ -47,7 +47,7 @@
         "ext-pdo": "*",
         "ext-mbstring": "*",
         "ext-dom": "*",
-        "opis/json-schema": "^1.0",
+        "opis/json-schema": "^1.1",
         "slim/psr7": "1.4",
         "slim/slim": "4.7.1",
         "php-di/php-di": "6.3.4",
diff --git a/composer.lock b/composer.lock
index ea090ceb84c62f1378138e8cc38004e23b89a97e..bf744a8e1d2f84975aa167a1b4dd6fc35359cc50 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "5cc4ba67bf29d9dec56b819f4a86e27b",
+    "content-hash": "4b35b69868f07e1e9694a8df66d0e4a9",
     "packages": [
         {
             "name": "algo26-matthias/idna-convert",
diff --git a/lib/classes/JsonApi/Ext/AtomicOperations/AddOperation.php b/lib/classes/JsonApi/Ext/AtomicOperations/AddOperation.php
new file mode 100644
index 0000000000000000000000000000000000000000..0d68ae676bc7e885ed46d906cd26266e5877794f
--- /dev/null
+++ b/lib/classes/JsonApi/Ext/AtomicOperations/AddOperation.php
@@ -0,0 +1,10 @@
+<?php
+namespace JsonApi\Ext\AtomicOperations;
+
+final class AddOperation extends Operation
+{
+    public function getMethod(): string
+    {
+        return 'POST';
+    }
+}
diff --git a/lib/classes/JsonApi/Ext/AtomicOperations/Operation.php b/lib/classes/JsonApi/Ext/AtomicOperations/Operation.php
new file mode 100644
index 0000000000000000000000000000000000000000..fa14ce15620321764ca453e8c2e61dd30ccfba69
--- /dev/null
+++ b/lib/classes/JsonApi/Ext/AtomicOperations/Operation.php
@@ -0,0 +1,37 @@
+<?php
+namespace JsonApi\Ext\AtomicOperations;
+
+use Psr\Http\Message\ServerRequestFactoryInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\StreamFactoryInterface;
+
+abstract class Operation
+{
+    abstract public function getMethod(): string;
+
+    /**
+     * @var mixed
+     */
+    protected $data;
+
+    /**
+     * @var string
+     */
+    protected $href;
+
+    final public function __construct(string $href, array $data = null)
+    {
+        $this->href = $href;
+        $this->data = $data;
+    }
+
+    public function getHref(): string
+    {
+        return $this->href;
+    }
+
+    public function getData()
+    {
+        return $this->data;
+    }
+}
diff --git a/lib/classes/JsonApi/Ext/AtomicOperations/OperationParser.php b/lib/classes/JsonApi/Ext/AtomicOperations/OperationParser.php
new file mode 100644
index 0000000000000000000000000000000000000000..62baa8b65ed05d0878fc2bfd19fad61e4b085a57
--- /dev/null
+++ b/lib/classes/JsonApi/Ext/AtomicOperations/OperationParser.php
@@ -0,0 +1,86 @@
+<?php
+namespace JsonApi\Ext\AtomicOperations;
+
+use Opis\JsonSchema\ISchema;
+use Opis\JsonSchema\Loaders\File;
+use Opis\JsonSchema\ValidationError;
+use Opis\JsonSchema\Validator;
+
+class OperationParser
+{
+    public static function create(): OperationParser
+    {
+        $loader = new File('', [__DIR__]);
+        $schema = $loader->loadSchema('/atomic-operations-schema.json');
+        return new static($schema);
+    }
+
+    /**
+     * @var ISchema
+     */
+    protected $schema;
+
+    /**
+     * @var Validator
+     */
+    protected $validator;
+
+    /**
+     * @var array
+     */
+    protected $operations = [];
+
+    private final function __construct(ISchema $schema)
+    {
+        $this->schema = $schema;
+
+        $this->validator = new Validator();
+    }
+
+    /**
+     * @param array $input
+     * @param Callable|null $onError
+     * @return bool
+     */
+    public function parse(object $input, Callable $onError = null): bool
+    {
+        $result = $this->validator->schemaValidation($input, $this->schema);
+
+        if (!$result->isValid()) {
+            if (is_callable($onError)) {
+                array_map(function (ValidationError $error) use ($onError) {
+                    foreach ($error->subErrors() as $e) {
+                        if ($e->keyword() !== 'const') {
+                            return $onError($e);
+                        }
+                    }
+                    return $onError($error);
+                }, $result->getErrors());
+            }
+            return false;
+        }
+
+        $temp = (array) $input;
+
+        $this->operations = [];
+        foreach ($temp['atomic:operations'] as $op) {
+            if ($op->op === 'add') {
+                $this->operations[] = new AddOperation($op->href, (array) $op->data);
+            } elseif ($op->op === 'update') {
+                $this->operations[] = new UpdateOperation($op->href, (array) $op->data);
+            } elseif ($op->op === 'remove') {
+                $this->operations[] = new RemoveOperation($op->href);
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * @return Operation[]
+     */
+    public function getOperations(): array
+    {
+        return $this->operations;
+    }
+}
diff --git a/lib/classes/JsonApi/Ext/AtomicOperations/RemoveOperation.php b/lib/classes/JsonApi/Ext/AtomicOperations/RemoveOperation.php
new file mode 100644
index 0000000000000000000000000000000000000000..1769c5e013857737c91dc5b4099d992b7cd15970
--- /dev/null
+++ b/lib/classes/JsonApi/Ext/AtomicOperations/RemoveOperation.php
@@ -0,0 +1,10 @@
+<?php
+namespace JsonApi\Ext\AtomicOperations;
+
+final class RemoveOperation extends Operation
+{
+    public function getMethod(): string
+    {
+        return 'DELETE';
+    }
+}
diff --git a/lib/classes/JsonApi/Ext/AtomicOperations/Route.php b/lib/classes/JsonApi/Ext/AtomicOperations/Route.php
new file mode 100644
index 0000000000000000000000000000000000000000..7163f19d57122a510b1e33ff911a4b6a17a65e9b
--- /dev/null
+++ b/lib/classes/JsonApi/Ext/AtomicOperations/Route.php
@@ -0,0 +1,226 @@
+<?php
+namespace JsonApi\Ext\AtomicOperations;
+
+use GuzzleHttp\Psr7\Utils;
+use Neomerx\JsonApi\Contracts\Http\Headers\MediaTypeInterface;
+use Neomerx\JsonApi\Exceptions\JsonApiException;
+use Neomerx\JsonApi\Schema\Error;
+use Neomerx\JsonApi\Schema\ErrorCollection;
+use Opis\JsonSchema\ValidationError;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Slim\App;
+use Slim\Psr7\Factory\ServerRequestFactory;
+use Slim\Psr7\Factory\StreamFactory;
+use Slim\Psr7\Factory\UriFactory;
+
+/**
+ * Should only respond to application/vnd.api+json;ext="https://jsonapi.org/ext/atomic"
+ * Should output application/vnd.api+json;ext="https://jsonapi.org/ext/atomic"
+ *
+ * @see https://jsonapi.org/ext/atomic
+ */
+final class Route extends \JsonApi\NonJsonApiController
+{
+    const MEDIA_TYPE = 'application/vnd.api+json;ext="https://jsonapi.org/ext/atomic"';
+
+    /**
+     * @var App
+     */
+    private $app;
+
+    /**
+     * @var OperationParser
+     */
+    private $parser;
+
+    /**
+     * @var StreamFactory
+     */
+    private $stream_factory;
+
+    /**
+     * @var ServerRequestFactory
+     */
+    private $request_factory;
+
+    public function __construct(ContainerInterface $container)
+    {
+        parent::__construct($container);
+
+        $this->app = $this->container->get(App::class);
+
+        $this->parser = OperationParser::create();
+        $this->stream_factory = new StreamFactory();
+        $this->request_factory = new ServerRequestFactory($this->stream_factory);
+    }
+
+    public function __invoke(Request $request, Response $response, array $args): Response
+    {
+        $json = json_decode(file_get_contents(__DIR__ . '/delete-me-before-merge.json'));
+        $request = $request
+            ->withHeader('Content-Type', self::MEDIA_TYPE)
+            ->withParsedBody($json);
+
+        $errors = new ErrorCollection();
+
+        $this->parser->parse(
+            $request->getParsedBody(),
+            function (ValidationError $error) use (&$errors) {
+                $errors->add($this->convertError($error));
+            }
+        );
+
+        if ($errors->count() > 0) {
+            throw new JsonApiException($errors, JsonApiException::HTTP_CODE_BAD_REQUEST);
+        }
+
+        $requests = array_map(
+            function (Operation $operation): Request {
+                return $this->request_factory->createServerRequest(
+                    $operation->getMethod(),
+                    $operation->getHref()
+                )->withHeader(
+                    'Content-Type',
+                    MediaTypeInterface::JSON_API_MEDIA_TYPE
+                )->withHeader(
+                    'Accept',
+                    MediaTypeInterface::JSON_API_MEDIA_TYPE
+                )->withBody(
+                    $this->stream_factory->createStream(json_encode($operation->getData()))
+                )->withParsedBody($operation->getData());
+            },
+            $this->parser->getOperations()
+        );
+
+        // Start transaction
+
+        try {
+            $responses = [];
+            foreach ($requests as $index => $request) {
+                $factory = new UriFactory();
+                $uri = $factory->createUri($this->app->getBasePath() . '/v1/discovery');
+                $request = $request->withUri($uri);
+                $request = $request->withMethod('GET');
+                $routeResolver = $this->app->getRouteResolver();
+                $routingResult = $routeResolver->computeRoutingResults(
+                    $request->getUri()->getPath(),
+                    $request->getMethod()
+                );
+                $route = $routeResolver->resolveRoute($routingResult->getRouteIdentifier());
+                dd($route->getCallable());
+                $responses[$index] = $route->handle($request);
+            }
+            // Commit transaction
+        } catch (\Exception $e) {
+            // Rollback transaction on error
+            throw new JsonApiException($e, JsonApiException::HTTP_CODE_BAD_REQUEST);
+        }
+
+        $result = [
+            'atomic:results' => array_map(function (Response $response) {
+                $body = $response->getBody();
+                if ($body->isSeekable()) {
+                    $body->rewind();
+                }
+                return json_decode($body->getContents(), true);
+            }, $responses),
+        ];
+
+        return $response
+            ->withHeader('Content-Type', self::MEDIA_TYPE)
+            ->withBody(Utils::streamFor(json_encode($result)));
+    }
+
+    private function getOperationsFromRequest(Request $request, ErrorCollection $errors): array
+    {
+        $json = $this->parseJSON($request->getBody());
+
+        if (!is_array($json) || array_is_list($json)) {
+            // $errors
+        }
+
+        $operations = [];
+        foreach ($json as $index => $value) {
+            if (!$this->isOperation($value)) {
+                $errors->add(new Error(
+                    'no-operation',
+                    null, null,
+                    null, null,
+                    'Value is not an operation',
+                    "The item at index {$index} of the input array is not an operation",
+                    ['index' => $index]
+                ));
+            } else {
+                $operations[] = $this->parseOperation($value);
+            }
+        }
+
+        if ($errors->count() > 0) {
+            throw new JsonApiException($errors, JsonApiException::HTTP_CODE_BAD_REQUEST);
+        }
+
+        return $operations;
+    }
+
+    /**
+     * @param string $input
+     * @return mixed
+     * @throws \JsonException
+     */
+    private function parseJSON(string $input)
+    {
+        $result = json_decode($input, true, 512);
+        $error = json_last_error();
+        if ($error) {
+            throw new \JsonException(json_last_error_msg(), $error);
+        }
+        return $result;
+    }
+
+    private function isOperation($value): bool
+    {
+        return true;
+    }
+
+    private function parseOperation($value): AtomicOperation
+    {
+        return new AtomicOperation();
+    }
+
+    private function convertError(ValidationError $error): Error
+    {
+        $i = -1;
+        $where = implode('', array_map(function ($what) use (&$i) {
+            $i += 1;
+            return is_numeric($what) ? "[{$what}]" : ($i ? ".{$what}" : $what);
+        }, $error->dataPointer()));
+
+        return new Error(
+            'validation-error',
+            null, null,
+            null, null,
+            $this->getErrorDescription($error),
+            $where,
+            ['index' => '?']
+        );
+    }
+
+    private function getErrorDescription(ValidationError $error): string
+    {
+        $what = $error->keyword();
+
+        if (count($error->keywordArgs()) > 0) {
+            $what .= ': ' . json_encode($error->keywordArgs());
+        }
+
+        if ($error->subErrorsCount() > 0) {
+            $what .= ' [';
+            $what .= implode(', ', array_map([$this, 'getErrorDescription'], $error->subErrors()));
+            $what .= ']';
+        }
+
+        return $what;
+    }
+}
diff --git a/lib/classes/JsonApi/Ext/AtomicOperations/UpdateOperation.php b/lib/classes/JsonApi/Ext/AtomicOperations/UpdateOperation.php
new file mode 100644
index 0000000000000000000000000000000000000000..94d233517746c1ceb0842ef5c9ab440b4cd7214f
--- /dev/null
+++ b/lib/classes/JsonApi/Ext/AtomicOperations/UpdateOperation.php
@@ -0,0 +1,10 @@
+<?php
+namespace JsonApi\Ext\AtomicOperations;
+
+final class UpdateOperation extends Operation
+{
+    public function getMethod(): string
+    {
+        return 'PATCH';
+    }
+}
diff --git a/lib/classes/JsonApi/Ext/AtomicOperations/atomic-operations-schema.json b/lib/classes/JsonApi/Ext/AtomicOperations/atomic-operations-schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..9ea97b0067386177bfb1d9363c023e366cdb48bf
--- /dev/null
+++ b/lib/classes/JsonApi/Ext/AtomicOperations/atomic-operations-schema.json
@@ -0,0 +1,64 @@
+{
+  "$comment": "https://jsonapi.org/ext/atomic/",
+
+  "type": "object",
+  "properties": {
+    "atomic:operations": {
+      "type": "array",
+      "items": {
+        "type": "object",
+        "anyOf": [
+          {
+            "properties": {
+              "op": {
+                "const": "add"
+              },
+              "href": {
+                "$comment": "This differs from the specification where this property is optional.",
+                "type": "string",
+                "pattern": "^\\/"
+              },
+              "data": {
+                "type": "object"
+              }
+            },
+            "required": ["op", "href","data"],
+            "allowAdditionalProperties": false
+          },
+          {
+            "properties": {
+              "op": {
+                "const": "update"
+              },
+              "href": {
+                "$comment": "This differs from the specification where this property is optional.",
+                "type": "string",
+                "pattern": "^\\/"
+              },
+              "data": {
+                "type": "object"
+              }
+            },
+            "required": ["op", "href", "data"],
+            "allowAdditionalProperties": false
+          },
+          {
+            "properties": {
+              "op": {
+                "const": "remove"
+              },
+              "href": {
+                "$comment": "This differs from the specification where this property is optional.",
+                "type": "string",
+                "pattern": "^\\/"
+              }
+            },
+            "required": ["op", "href"],
+            "allowAdditionalProperties": false
+          }
+        ]
+      }
+    }
+  },
+  "additionalProperties": false
+}
diff --git a/lib/classes/JsonApi/Ext/AtomicOperations/delete-me-before-merge.json b/lib/classes/JsonApi/Ext/AtomicOperations/delete-me-before-merge.json
new file mode 100644
index 0000000000000000000000000000000000000000..067381d6893a19f8a647ed032a08e5b3a6c62ec2
--- /dev/null
+++ b/lib/classes/JsonApi/Ext/AtomicOperations/delete-me-before-merge.json
@@ -0,0 +1,22 @@
+{
+  "atomic:operations": [
+    {
+      "op": "add",
+      "href": "/blogs",
+      "data": {
+        "foo": 42
+      }
+    },
+    {
+      "op": "update",
+      "href": "/blogs/1",
+      "data": {
+        "foo": 23
+      }
+    },
+    {
+      "op": "remove",
+      "href": "/blogs/1"
+    }
+  ]
+}
diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php
index 0709ef3a75d82e7c760d22c5b2b59e1b6c9bac92..322ecbe0ec744343d306ec72836d6f0eee15e2e3 100644
--- a/lib/classes/JsonApi/RouteMap.php
+++ b/lib/classes/JsonApi/RouteMap.php
@@ -5,8 +5,6 @@ namespace JsonApi;
 use JsonApi\Contracts\JsonApiPlugin;
 use JsonApi\Middlewares\Authentication;
 use JsonApi\Middlewares\DangerousRouteHandler;
-use JsonApi\Middlewares\JsonApi as JsonApiMiddleware;
-use JsonApi\Middlewares\StudipMockNavigation;
 use Slim\Routing\RouteCollectorProxy;
 
 /**
@@ -84,6 +82,9 @@ class RouteMap
         $group->group('', [$this, 'unauthenticatedRoutes']);
 
         $group->get('/discovery', Routes\DiscoveryIndex::class);
+        $group->post('/operations', Ext\AtomicOperations\Route::class);
+        // TODO: Remove next, debug only
+        $group->get('/operations', Ext\AtomicOperations\Route::class);
     }
 
     /**
@@ -561,4 +562,3 @@ class RouteMap
         $group->map(['GET', 'PATCH', 'POST', 'DELETE'], $url, $handler);
     }
 }
-
diff --git a/tests/_data/jsonapi-atomic-operations/invalid/invalid-operation.json b/tests/_data/jsonapi-atomic-operations/invalid/invalid-operation.json
new file mode 100644
index 0000000000000000000000000000000000000000..12bb8a91fe2c0c528adfdeb71776f9a1bfe5a1ae
--- /dev/null
+++ b/tests/_data/jsonapi-atomic-operations/invalid/invalid-operation.json
@@ -0,0 +1,7 @@
+{
+  "atomic:operations": [
+    {
+      "op": "invalid"
+    }
+  ]
+}
diff --git a/tests/_data/jsonapi-atomic-operations/invalid/invalid-root.json b/tests/_data/jsonapi-atomic-operations/invalid/invalid-root.json
new file mode 100644
index 0000000000000000000000000000000000000000..21cf573ae7b4f964de1b422650190a907c9160b9
--- /dev/null
+++ b/tests/_data/jsonapi-atomic-operations/invalid/invalid-root.json
@@ -0,0 +1,4 @@
+{
+  "atomic:operations": [],
+  "atomic:results": []
+}
diff --git a/tests/_data/jsonapi-atomic-operations/valid/operations-empty.json b/tests/_data/jsonapi-atomic-operations/valid/operations-empty.json
new file mode 100644
index 0000000000000000000000000000000000000000..c9b94e9bc7d92c80b144b177a5e513de6c9b879a
--- /dev/null
+++ b/tests/_data/jsonapi-atomic-operations/valid/operations-empty.json
@@ -0,0 +1,3 @@
+{
+  "atomic:operations": []
+}
diff --git a/tests/_support/_generated/JsonapiTesterActions.php b/tests/_support/_generated/JsonapiTesterActions.php
index 9faaeec96dbbe35f2edf4acde9118df8e197d724..722a32c4286adeeb2d277213a8b2a9ecfd7ccfe7 100644
--- a/tests/_support/_generated/JsonapiTesterActions.php
+++ b/tests/_support/_generated/JsonapiTesterActions.php
@@ -1,4 +1,4 @@
-<?php  //[STAMP] bc9d12565d6304b308d375b50b9e8556
+<?php  //[STAMP] d9421758f0cb24a6ac9e5ddde03c18a3
 namespace _generated;
 
 // This class was automatically generated by build task
diff --git a/tests/jsonapi/AtomicOperationsParserTest.php b/tests/jsonapi/AtomicOperationsParserTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..15a5b27c8f8c21633cb1a15a6feab4da6351a154
--- /dev/null
+++ b/tests/jsonapi/AtomicOperationsParserTest.php
@@ -0,0 +1,68 @@
+<?php
+class AtomicOperationsParserTest extends \Codeception\Test\Unit
+{
+    protected function getErrorCollector()
+    {
+        return new class implements Countable {
+            public $errors = [];
+            public function __invoke($error)
+            {
+                $this->errors[] = $error;
+            }
+
+            public function count(): int
+            {
+                return count($this->errors);
+            }
+        };
+    }
+
+    /**
+     * @dataProvider validInputProvider
+     */
+    public function testValidInputs(object $input): void
+    {
+        $collector = $this->getErrorCollector();
+
+        $parser = \JsonApi\Ext\AtomicOperations\OperationParser::create();
+        $parser->parse($input, $collector);
+
+        $this->assertCount(0, $collector);
+    }
+
+    /**
+     * @dataProvider invalidInputProvider
+     */
+    public function testInvalidInputs(object $input): void
+    {
+        $collector = $this->getErrorCollector();
+
+        $parser = \JsonApi\Ext\AtomicOperations\OperationParser::create();
+        $parser->parse($input, $collector);
+
+        $this->assertGreaterThan(0, count($collector));
+    }
+
+    public static function validInputProvider(): array
+    {
+        return self::loadInputsFromDirectory(TEST_FIXTURES_PATH . '/jsonapi-atomic-operations/valid');
+    }
+
+    public static function invalidInputProvider(): array
+    {
+        return self::loadInputsFromDirectory(TEST_FIXTURES_PATH . '/jsonapi-atomic-operations/invalid');
+    }
+
+    protected static function loadInputsFromDirectory(string $directory): array
+    {
+        $result = [];
+        foreach (glob("{$directory}/*.json") as $filename) {
+            $result[basename($filename, '.json')] = [
+                json_decode(file_get_contents($filename)),
+            ];
+        }
+
+        return $result;
+
+    }
+}
diff --git a/tests/jsonapi/_bootstrap.php b/tests/jsonapi/_bootstrap.php
index d9c5adcef74f6097bcfd246aef501bd59a62da93..383ae0d378126439b49508199981b7fda9ce9433 100644
--- a/tests/jsonapi/_bootstrap.php
+++ b/tests/jsonapi/_bootstrap.php
@@ -97,4 +97,6 @@ class DB_Seminar extends DB_Sql
     }
 }
 
+define('TEST_FIXTURES_PATH', dirname(__DIR__) . '/_data/');
+
 require_once __DIR__.'/../../composer/autoload.php';