Writing API Endpoints - HL Vanilla Community
<main> <article class="userContent"> <div class="embedExternal embedImage display-large float-none"> <div class="embedExternal-content"> <a class="embedImage-link" href="https://us.v-cdn.net/6030677/uploads/SV7U3H1MJ4F3/microsoftteams-image-288-29.png" rel="nofollow noreferrer noopener ugc" target="_blank"> <img class="embedImage-img" src="https://us.v-cdn.net/6030677/uploads/SV7U3H1MJ4F3/microsoftteams-image-288-29.png" alt="MicrosoftTeams-image (8).png" height="108" width="1356" loading="lazy" data-display-size="large" data-float="none"></img></a> </div> </div> <h2 data-id="controller-endpoints">Controller Endpoints</h2><p>When writing an API controller class, each method represents an endpoint. How to define those endpoint names and parameters is covered in the <a href="https://docs.vanillaforums.com/developer/apiv2/resource-routing" rel="nofollow noreferrer ugc">resource routing</a> guide. This guide concerns writing the contents of a method.</p><h2 data-id="the-controller-base-class">The Controller Base Class</h2><p>Although controllers don’t need to inherit from any class, the <strong>Vanilla\Web\Controller</strong> class offers useful functionality and is going to be the class you inherit from almost 100% of the time. This guide assumes you are inheriting from that class and using its utility methods.</p><h2 data-id="dependency-injection">Dependency Injection</h2><p>Most controllers are thin wrappers that add permission checks around calls to models that do most of the real work. The dispatcher creates all controllers using a dependency injection container, so you can have your controller’s models and other support objects properly initialized by declaring type-hinted parameters in your controller’s constructor.</p><p>The controller base class also has some default dependencies that you can also use:</p><ul><li><strong>getSession()</strong>. This is the session object corresponding to the user that invoked the controller.</li><li><strong>getEventManager()</strong>. This is the event manager and can be used to fire events from within the controller.</li></ul><h2 data-id="the-anatomy-of-a-controller-action">The Anatomy of a Controller Action</h2><p>When writing a controller action, your methods are going to have a similar layout. Take this post action as an example:</p><pre class="code codeBlock" spellcheck="false" tabindex="0">public function post(array $data) { // Check permissions. $this->permission('...'); // Define the input schema. $in = $this->schema([ ... ], __FUNCTION__); // Define the output schema. $out = $this->schema([ ... ], __FUNCTION__, 'out'); // Validate the input data against the input schema. $row = $in->validate($data); // Do the controller's job. $result = $this->model->insert($row); // Trim the full result to the output schema. $result = $out->validate($result); // Return the result. return $result; } </pre><h3 data-id="check-permissions">Check Permissions</h3><p>The first line of most actions is a call to <code class="code codeInline" spellcheck="false" tabindex="0">$this->permission()</code>. This applies even if your endpoint doesn’t require any specific permissions and has few exceptions. If you don’t think your endpoint requires a permission then just make the call with an empty string:</p><pre class="code codeBlock" spellcheck="false" tabindex="0">$this->permission(''); // no specific permission required </pre><p>We want to strictly enforce a permission call because there can be other reasons why a user may not have permissions such as a site being in update mode or being a private community. Such permission restrictions are known as bans. There are several default bans listed as constants on the <strong>Vanilla\Permissions</strong> class. You can bypass a ban by specifying it as a permission name in your call to the <strong>permission()</strong> method.</p><pre class="code codeBlock" spellcheck="false" tabindex="0">// This endpoint is still available when the site is in update mode. $this->permission(['Vanilla.Discussions.View', Permissions::BAN_UPDATING]); </pre><h4 data-id="checking-multiple-permissions">Checking Multiple Permissions</h4><p>If an endpoint requires multiple permissions you can make several calls to <strong>permission()</strong>. Try and put the most “important” permission first, but the only impact the order of the calls has is on the order of error messages if the user doesn’t have either permission.</p><p>If an endpoint is available to a user with <strong>any</strong> of a set of permissions then specify all of them as an array in one call to <strong>permission()</strong>.</p><h4 data-id="later-permission-checks">Later Permission Checks</h4><p>You can make calls to <strong>permission()</strong> later in the action. The most common reason to do this would be to lookup a record before deciding what permission the user needs. Even if you make a permission call later like this you should still include an initial call to <strong>permission()</strong>. <em>Always be thinking about protecting your endpoints!</em></p><h3 data-id="define-the-input-schema">Define The Input Schema</h3><p>The input schema is important for the following reasons:</p><ol><li>It cleans your input data beyond what JSON can do. For example, dates are converted into <strong>DateTimeImmutable</strong> objects.</li><li>It helps define your API’s specification. Proper APIs should be backwards-compatible whenever new features are added. The schema helps ensure you support your old consumers.</li><li>It helps secure your endpoint using a whitelist of what’s allowed. This means you don’t have to worry about extra database fields sneaking in and overwriting sensitive data (such as admin flags). Ask any security expert and they’ll tell you that whitelist security is preferred to blacklist security.</li><li>It documents your endpoint. The schema is used to generate automatic documentation, but beyond that it’s also useful to other developers that are modifying your endpoint.</li></ol><p>Schemas returned from the <strong>schema()</strong> method are instances of the <strong>Vanilla\Schema</strong> class which is a thin subclass of the <strong>Garden\Schema</strong> class that adds some meta information useful for endpoint documentation and events for extension.</p><h3 data-id="define-the-output-schema">Define The Output Schema</h3><p>The output schema isn’t as important a the input schema, but a properly specified API should have both. The output schema’s most important role is for documentation. It also helps trim unnecessary data from the result.</p><p>It may seem strange to define the output schema right below the input schema instead of where it is used. However, this is done to aid in automated documentation generation.</p><h3 data-id="validate-the-input-data">Validate The Input Data</h3><p>Validating the input is as easy as calling the the schema’s <strong>validate()</strong> method. This does the following:</p><ol><li>The data is validated. If the validation fails then an exception is thrown that the dispatcher understands how to render.</li><li>The data is cleaned. Values are coerced to proper types and extraneous fields are stripped. This leaves the resulting data suitable for use without worrying about bad data.</li></ol><h3 data-id="do-the-controllers-job">Do the Controller’s Job</h3><p>The controller’s main job is done after permission checking and input validation. What the controller does is up to you, but try keeping it simple. If you controller is overly complex think of whether that means you need to instead add functionality to your model. If you aren’t sure maybe refactor to a private method on the controller so that the actual endpoint keeps its thin wrapper status.</p><h3 data-id="trim-the-result">Trim The Result</h3><p>You trim the result by validating the output schema. This may also have the effect of throwing an error. In this case you’ve done something wrong. Hopefully, such errors come out in unit testing and not in production.</p><h3 data-id="return-the-result">Return The Result</h3><p>In API controllers the result is returned rather than being rendered directly. In fact, the base controller has no <strong>render()</strong> method at all. The result is rendered by the dispatcher.</p><h4 data-id="the-data-class">The Data Class</h4><p>Usually, you will return an array which is easily passed to other functions or rendered to JSON. Returning an array in this way represents a 200 response. If you want to return some other response code you can return a <strong>Garden\Web\Data</strong> object that takes an HTTP status code as an argument in its constructor. The Data class also implements array access so it’s fairly easy to move from an array to an instance of this class.</p><p>You can also return an object that implements <strong>JsonSerializable</strong>. An object like this that doesn’t have any other specific information will only renderable as JSON which isn’t as forwards-compatible as the other classes so be careful here. The Data class isn’t in its final form and it will get more and more support for advanced scenarios in the future.</p><h2 data-id="tips-and-tricks">Tips And Tricks</h2><ul><li>Try and not reference any global objects or static methods in your controllers. Use dependency injection. We are trying to wrestle globals out of our application. This won’t be 100% possible at first, but keep the theory in the back of your mind.</li><li>Clever, yet lazy developers may notice that action methods have just gotten a lot longer than previous API versions. This is all for good reason and I hope this guide lays that out quite clearly. We want a good, testable, secure API. The only way to achieve that is to have the endpoints properly specified. This will pay dividends in the long term. Okay this isn’t exactly a tip or a trick, but the clever yet lazy developer is likely to scroll and read only this section.</li><li>Still, defining schemas is probably going to be the most arduous task. If you define a method that returns the output schema for your <strong>get()</strong> endpoint then you’ll be able to use that same schema as the row format of your <strong>index()</strong> endpoint:</li></ul><pre class="code codeBlock" spellcheck="false" tabindex="0">public function getRowSchema($method) { return $this->schema([ ... ], $method, 'out'); } // In your index() endpoint: $out = $this->schema([ '*:a' => $this->getRowSchema('') ], __FUNCTION__, 'out'); // In your get() endpoint: $out = $this->getRowSchema(__FUNCTION__); </pre><ul><li>Remember to always make the initial call to <strong>permission()</strong>. <em>Protect that endpoint!</em></li><li>You may have noticed that the examples above have odd names for the schema variables. That’s because variables with three characters indent nicely with method chaining, and schemas are very likely to method chain. Don’t hate on the 3LVs.</li></ul><p><br></p> </article> </main>