One place for hosting & domains

      An Introduction to the WordPress REST API


      When the REST API was finally added to WordPress core, it was the end of a long journey. Many had anticipated this change as the biggest step forward for WordPress in the platform’s history. However, if you’re not familiar with the REST API, you may be confused by what it all means.

      In short, the addition of the WordPress REST API turned WordPress into a fully-featured application framework. This significantly increased its ‘extensibility,’ or its ability to be extended with new features and capabilities. Plus, it expanded the platform’s potential for communicating with other sites and applications.

      An Introduction to REST APIs

      Before we dig deeper into the WordPress REST API, it’s important to get our terminology straight. This is a subject where we’ll need to use a lot of acronyms, so let’s clear those up first.

      First and foremost, you’ll need to know what Application Programming Interfaces (APIs) are. In the simplest terms, an API is a means by which one system enables other systems to connect to its data.

      For example, when a website adds a Facebook ‘like’ button to a page, it does this by hooking into Facebook’s API. This lets the web page use the API to receive data (the code for the like button) and send data (the like request).

      So, what is a REST API specifically? Representational State Transfer (REST) is a type of API specific to web services. It contains a standardized set of instructions and rules, making it easier for all ‘RESTful’ services to connect with each other.

      In short, REST APIs enable you to make requests to an external system. One example of this is Twitter. You can use its API to request a certain number of tweets from a specific user. The API will then return the tweets based on your request, which you can embed on your site using HTML and CSS.

      These requests are carried out using JavaScript Object Notation (JSON). This is a language specifically designed for sending, receiving, and storing data.

      We’re going to cover JSON later in this article, but we recommend taking the time to familiarize yourself with this language upfront. This will help prime you for using the WordPress REST API and understanding some of the concepts we’ll be talking about.

      What the WordPress REST API Is (And Why It’s Important)

      WordPress Rest API

      The WordPress REST API functions in largely the same way as the examples we’ve touched on already. Basically, the WordPress REST API gives you full access to WordPress features from any JSON-compatible framework.

      Similarly to how Twitter’s API enables you to retrieve and send tweets, the WordPress REST API can be used to manage posts, users, categories, and much more from external platforms. It lets you use WordPress in a number of previously unprecedented ways.

      The REST API was announced all the way back in 2013. It started life as a plugin, meant to be incorporated into the WordPress core by Version 4.1. As so often happens, delays pushed the release back until it was finally implemented into the core with the release of WordPress 4.7 three years later.

      This was a long but worthwhile wait for many people who saw the WordPress REST API as an important step forward for the platform. You might be wondering why this addition was such a big deal, especially since a lot of users probably didn’t notice much difference. As it turns out, the inclusion of the REST API was a fundamental change to WordPress for many reasons.

      By implementing a REST API, WordPress took a step away from simply being a platform for creating websites. Instead, it’s now become a full-fledged application framework. This means developers can use a WordPress site to create applications for mobile devices and the web or as an information repository.

      This shift also enabled WordPress to take a step away from its reliance on PHP. By making WordPress compatible with any JSON-compatible language, the REST API greatly expanded the possibilities for developers, enabling them to use WordPress functionality with practically any framework.

      Finally, the REST API provides increased flexibility with the interfaces you can use to work with the platform. It made the admin interface completely optional since you can now interact with your WordPress site entirely through JSON commands.

      Now, let’s look at how JSON and the REST API come together to make this possible.

      How the REST API and JSON Work Together

      By now, you should have a handle on the theoretical aspects of the WordPress REST API. So, let’s look at the more practical side of the technology. The official handbook describes using the REST API as follows:

      “The WordPress REST API provides API endpoints for WordPress data types that allow developers to interact with sites remotely, by sending and receiving JSON (JavaScript Object Notation) objects.”

      The first word we need to focus on here is “endpoints”. The easiest way to think of an endpoint is as a piece of data or a function that can be called using a JSON request. By default, WordPress provides a huge number of standard endpoints to use, but developers can also create custom endpoints.

      To reach an endpoint, you must use a ‘route,’ which takes the form of a normal URL. You can even try this yourself right now.

      Go to your own WordPress site, and add /wp-json/wp/v2 to the end of its URL. If your site is http://example.com, you would enter http://example.com/wp-json/wp/v2.

      When you load this route, you will reach the endpoint, which in this case, returns all content and meta-data for your site in a (messy) JSON format. By using different routes, you can access different endpoints to get specific types of information and perform various tasks.

      There are three primary JSON requests you will use with the REST API, so let’s also take a quick look at them now. They are:

      • GET. This type of request is used for retrieving and listing data from the API. For example, you would use a GET request to return a list of users on your site or compile blog posts from a certain timeframe.
      • POST. This request is used for sending data to the API. It enables you to push new information to WordPress, such as adding new users and posts or updating existing data.
      • DELETE. As the name suggests, this request is used to delete data. This enables you to remove posts, pages, users, and more.

      GET and POST can sometimes be used with the same endpoint to achieve different results.

      For example, let’s look at the endpoint /me/settings/. If you were to perform a GET request on this endpoint, you would receive a list of the current user’s settings. However, by using a POST request on the same endpoint, you would be able to update the settings instead.

      Get Content Delivered Straight to Your Inbox

      Subscribe to our blog and receive great content just like this delivered straight to your inbox.

      Getting Started with the WordPress REST API

      We’re now going to put all of this theory into practice and show you some very basic examples of what you can do with the REST API. This is only a taste to help you become comfortable using the REST API to process requests to WordPress.

      For more examples, we recommend checking out the official reference library and the REST API Resources.

      The following techniques will require you to use the command line to process JSON requests. This enables you to interact with your WordPress site by using a text-based interface and sending simple commands.

      If you don’t have any experience using the command line, we recommend taking some time to learn the basics first. You may also want to use SSH to create the connection with your site.

      Finally, when you’re ready, let’s look at some examples of how you can use the WordPress REST API!

      1. Return Posts from a Site

      While you will obviously need the proper authorization to edit a website, it’s possible to retrieve some information from almost any WordPress site. This is because the REST API is consistent across all WordPress installations.

      As we discussed, the main reason that APIs exist is to enable external applications to access some of your data. In this example, we can retrieve a single post from the official WordPress news blog:

      curl https://wordpress.org/news/wp-json/wp/v2/posts/1

      The ID has been set to 1, meaning that this request will retrieve the very first post on the blog. It might be hard to see since the JSON is not very readable, but among the code, you can spot all the content and meta-data for the post:

      retrieve a post from the WordPress blog using the WordPress Rest API

      You could then use this information in an application, for example, to display it using your own customized styling.

      If you want to return every post from the blog instead, all you have to do is remove the ID at the end. However, it’s more likely that you’ll want to return a select number of posts. The following request will return the latest three posts:

      curl https://wordpress.org/news/wp-json/wp/v2/posts/?per_page=3

      You can try this out for yourself with other sites, and even your own blog.

      2. Update a Post

      Now, let’s try to make some changes to WordPress using the REST API. To do this, you will need to be logged in to the site you want to manage. For example, if you’re using SSH, you will need to log in to your server.

      In this example, we’ll update an existing post. First, let’s use a request to update the title of the post with the ID of 1:

      curl -X POST http://example.com/wp-json/wp/v2/posts/1 -d '{"title":"A Brand New Title"}'

      This is pretty self-explanatory. The title argument shows that you’re updating the post’s title, which is followed by the text string containing the replacement.

      There are plenty of other arguments you can use to make changes to a post. For instance, you can use a list to assign categories to the post, publish it, or change its contents entirely.

      3. Delete a User

      Finally, let’s look at how you can remove data using the REST API. In this example, we’ll remove a user from the site. Naturally, you’ll need to be logged in and authorized to manage users before you can use this function.

      Then, you can use the following request to delete the user with an ID of 101:

      curl -X DELETE http://example.com/wp-json/wp/v2/users/101

      This will remove the specified user from the site. You can use the additional parameters to reassign the user’s posts to another user based on their ID. Alternatively, you can force a permanent deletion instead of adding the user to the trash.

      Through these examples, you can start to see how the REST API enables you to manage the content on your site and connect to others. If you want to learn more, we recommend digging deeper into the REST API Handbook.

      Explore WordPress Development

      The WordPress REST API was a huge step forward for the platform, away from its roots and into the future. Developers were excited from day one, but if you weren’t familiar with REST APIs to begin with, you might have been confused about why.

      Although the REST API might seem overwhelming for beginners, you don’t need to be an experienced developer to use some basic requests. For example, the API enables you to perform diverse tasks on your own site (or others), such as returning posts, updating posts, and deleting users.

      Are you looking for high-performance hosting for your WordPress site? At Dreamhost, our DreamPress managed plans offer professional staging environments, automatic backups, in-built caching, and more. Check out our plans today!

      Do More with DreamPress

      DreamPress’ automatic updates, caching, and strong security defenses take WordPress management off your hands so you can focus on your website.

      Managed WordPress Hosting - DreamPress



      Source link

      How to Create Documentation for Your REST API with Insomnia


      The author selected the COVID-19 Relief Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      In this tutorial, you will document your API using the OpenAPI specification (v3). An OpenAPI file is a JSON or YAML file that follows the OpenAPI specification. This specification defines what fields your JSON/YAML file must contain and how it will be reflected on the documentation service you’ll use to host it. Many services support OpenAPI, so you can pick and choose, or even use multiple services, without having to change your API documentation’s format.

      To create the documentation, you’ll use Insomnia, a free and open-source application that allows you to test your API and design the documentation with a real-time side-by-side preview. Insomnia doesn’t support JSON, but it does make it easy to write YAML. YAML is a good choice for API documentation because these documents can get very large, and a JSON document would get cluttered and hard to read.

      Finally, you’ll host the API documentation with Redoc, an open-source application used by many companies. Redoc takes the OpenAPI document you generated and gives you an HTML page that displays a nice-looking and interactive version of your documentation. You’ll also deploy your Redoc generated site to GitHub Pages, which is a free website hosting solution by GitHub.

      In this tutorial, you will learn more about OpenAPI, document your API according to the OpenAPI Spec in Insomnia, and host this documentation on GitHub Pages with Redoc.

      Prerequisites

      To follow this tutorial, you will need:

      Step 1 — Understanding Your API

      In this step, you’ll note the routes your API accepts and their relevant parameters and responses. Since you’ll be documenting the API for others, and because you may also refer back to this documentation in the future, it’s important to note everything you need to document. OpenAPI allows you to define request bodies, headers, cookies, and even possible responses for each API route.

      This tutorial will use the JSON Placeholder API, which is a free mock API. Since this API is quite large, you will only be documenting the /posts section in this tutorial.

      The below table shows the method, route path, and description of each of the five routes you will document in this tutorial. Making a table or something similar is helpful so that you don’t forget any route (which can happen if the API is big).

      MethodRouteDescription
      GET/postsGet all posts
      GET/posts/:idGet a single post
      POST/postsCreate a post
      PUT/PATCH/posts/:idUpdate a post
      DELETE/posts/:idDelete a post

      Now that you know what your API can do, it’s time to begin documenting it with Insomnia.

      Step 2 — Creating an Insomnia Project

      In this step, you’ll create an Insomnia project. An Insomnia project contains the OpenAPI document, any tests you write for your API, and any requests you’ve created. The interface is split into three tabs: Design, Test, and Debug. You’ll focus on the design tab for this tutorial.

      Open the Insomnia app and go to your dashboard. Create a new Design Document by clicking the Create button on the top right of the Insomnia window and give it a name. For this tutorial, you can use json-placeholder-docs.yaml.

      Note: YAML by design only accepts spaces as indentation. Insomnia, however, indents with tabs by default. This will be fixed in a later update, but for now, open your Preferences by clicking the cogwheel icon on the top right, or by pressing Ctrl/Cmd + ,. In the Font section, uncheck Indent with Tabs and close the Preferences window. This will make Insomnia use spaces instead of tabs.

      You should now see three panes, as shown in the following screenshot below. The first pane shows an overview of your document, such as the routes of your API and components you’ve defined (you’ll learn more about those later). The middle pane contains the code editor that you’ll use to write the OpenAPI document in YAML. This editor also detects errors automatically and notifies you of them at the bottom. Finally, the last pane on the right is a real-time preview of the document. You’ll see an error because you still have to tell Insomnia which version of OpenAPI you’ll be using.

      Screenshot of Insomnia showing three panes. The first two panes are blank. The third pane on the right shows an error:

      In the code editor of the Design tab, add the line: openapi: 3.0.3. This indicates the version of the OpenAPI spec you will be using. At the time of writing, the latest version is 3.0.3. Feel free to change this to a later version if you’d like.

      Your screen should look similar to this:

      Screenshot of Insomnia showing one line added to the center pane, which is the code editor.

      Now that you’re familiar with the Insomnia interface, you can begin writing your documentation.

      Step 3 — Getting Started With the OpenAPI Specification

      In this step, you’ll learn more about the OpenAPI Specification. An API Specification can be a JSON or YAML file, but Insomnia only supports YAML. It should have a key called openapi that specifies the version of the OpenAPI Specficiation you’re using.

      According to the specification, here are the fields that can be present at the root of the document:

      NameTypeDescription
      openapistringREQUIRED. Version of the OpenAPI schema.
      infoInfo ObjectREQUIRED. An object containing information about the API.
      serversArray of Server ObjectsAn array containing objects that provide connectivity options to an API server.
      pathsPaths objectsREQUIRED. An object containing the routes provided by the API, methods, request-bodies, parameters and responses. This is the most important part of the document.
      componentsComponents ObjectContains reusable components, meant to reduce file size and keep the docs clean.
      securityArray of Security ObjectsContains a list of authentication mechanisms for the API. Outside the scope of this tutorial.
      externalDocsExternal Documentation ObjectContains any external documentation for the API

      Don’t worry if this is too much to take in. You’ll be diving deeper into each property, except for security, since that’s outside the scope of this tutorial. The security field defines authentication methods (e.g., username/password, JSON Web Token, or oauth) for a route, but JSONPlaceholder doesn’t have any authentication features.

      Step 4 — Adding the info Object

      In this step, you’ll use the table from Step 1 to begin write your API’s documentation using Insomnia. You’ll start with the info object.

      The info object contains information about the API you’re documenting. This includes things like the title, version of the API, the API’s description, links to its knowledge base (documentation), and its terms-of-service (tos).

      According to the specification, this is what an info object should look like:

      NameTypeDescription
      titlestringREQUIRED. The title of the API.
      descriptionstringA short description of the API. Markdown can be used here.
      termsOfServicestringA URL to the Terms of Service for the API.
      contactContact ObjectThe contact information for the exposed API.
      licenseLicense ObjectThe license information for the exposed API.
      versionstringREQUIRED. The version of the documentation, not the OpenAPI spec.

      The info field has two required properties: the title of the document and the version of the documentation, which should be equal to the version of your API application. The other fields are present for informing the user about your API.

      Now you will add an info object to your documentation using the three most-used fields: title, description, and version. In the Insomnia app, add the following YAML code to Design tab editor:

      info:
        title: JSONPlaceholder
        description: Free fake API for testing and prototyping.
        version: 0.1.0
      

      This is a random version number since JSONPlaceholder doesn’t expose a version number. Feel free to add any other fields to the info object, following the specification from the previous step.

      Warning: YAML is very picky about its indentation. It has to be indented with spaces, and the indent size must be consistent throughout the document.

      Now that you’ve added the info object with basic information about your API, you’ll add the next object: externalDocs.

      Step 5 — Adding the externalDocs Object

      In this step, you will add the externalDocs object. This object contains the link to any other documentation the API might have. An OpenAPI document just defines any routes your API has along with its parameters and responses, so it is usually used as a reference. It is recommended to include separate, human-generated docs that explain each action and guides the user. In JSONPlaceholder’s case, there is a guide.

      According to the specification, here’s what the externalDocs object should look like:

      Field NameTypeDescription
      descriptionstringA short description of the target documentation. Markdown can be used.
      urlstringREQUIRED. The URL for the target documentation.

      In your YAML document, add an externalDocs object that points to JSONPlaceholder’s guide:

      externalDocs:
        description: "JSONPlaceholder's guide"
        url: https://jsonplaceholder.typicode.com/guide
      

      You should see the changes reflected in the preview pane on the right side of Insomnia.

      Screenshot of Insomnia's preview, showing **JSONPlaceholder**.

      You have now linked to external documentation for your API. Next, you’ll add the servers array.

      Step 6 — Adding the servers Array

      In this step, you’ll add the servers array, which contains any URLs that the API will be hosted at. The documentation you’re creating will be hosted on a different domain from the placeholder API (that is, your documentation will not be hosted on jsonplaceholder.typicode.com). Because of this, you can’t implicitly get the URL for the Try It Out buttons next to the API routes shown in the Insomnia preview.

      To fix this, OpenAPI provides a servers field. Add the following lines to your YAML document:

      servers:
      - url: https://jsonplaceholder.typicode.com
        description: JSONPlaceholder
      

      With that, you now have a way to call the API.

      Step 7 — Adding the paths Object

      In this step, you will add the paths object, which is the heart of your documentation. This object contains all of the routes that are provided by the API. It also contains any parameters, the method, the request body, and all responses of the route.

      Each key of the paths object will be a route (/posts) and the value will be the Path Item object.

      According to the OpenAPI specification, this is what the Path Item object will look like:

      NameTypeDescription
      summarystringAn optional summary of this route.
      descriptionstringAn optional description of what the route can do.
      get/post/put/patch/delete/etcOperation ObjectA definition of an operation (method) on this route.
      serversArray of Server ObjectsAn alternative server array to service all operations in this path.
      parametersAn array of Parameter ObjectParameters that are applicable for all operations on this path. These parameters can be on the querystring, header, cookie, or the path itself.

      The Path Item object has a number of fields. The summary and description fields, as their names suggest, provide a short summary and longer description of the path. The servers object is the same as the one in the main OpenAPI document. It defines alternative servers. The parameters object defines any path or query parameters for that path. Each Path Item object can have an operation object. The operation object documents an HTTP method that can be used on this API route.

      The operation object has many items, but for this tutorial, you’ll focus on a smaller set:

      NameTypeDescription
      tagsArray of stringsA list of tags for API documentation control. Tags can be used for grouping similar routes.
      summarystringA short summary of what the operation does.
      descriptionstringA description of the operation. Markdown can be used here.
      externalDocsExternal Documentation ObjectAdditional external documentation for this operation. Same as externalDocs on the main object.
      parametersArray of Parameter ObjectsSame as parameters in the Path Item object.
      requestBodyRequest Body ObjectThe body of the request. This can NOT be used when the method GET or DELETE.
      responsesResponses ObjectREQUIRED. The list of possible responses returned by the API for this operation.

      The tags property groups similar paths. Paths with the same tag will end up in one group. The summary and description fields are the same as the ones in the path object. They allow you to add a short summary and a longer description, respectively. The externalDocs property is the same as that in the main document: it allows you to define any external documentation for that operation.

      The parameters object is the same as the one in the path object. It allows you to define path, query, header, or cookie parameters that have to be sent with the request. The requestBody also allows you to define parameters, but in the body of the request. This requestBody field is only available in POST, PUT and PATCH requests, as defined in the HTTP/1.1 protocol, RFC7231.

      The /posts Route

      Now you will document an API route by creating an object in the paths object. First, you’ll document the /posts route. Begin by adding these lines to your YAML document:

      paths:
        "/posts":
      

      Note: /posts is in quotes because it contains special symbols (/). This is required by YAML so it doesn’t misinterpret the line.

      Next, you need to add a field whose key will be the HTTP method, and whose value will be the Path Item object. Document the GET /posts route, which returns an array of all posts, by adding the highlighted lines:

      paths:
        "/posts":
          get:
            tags: ["posts"]
            summary: Returns all posts.
      

      The tags field groups similar operations together. (Notice how the accordion in the preview is called posts.)

      Next, document the responses one can get back. The only response you’ll get from this API is a 200 response containing an array of all posts.

      An example post that can be returned by JSONPlaceholder will look like this:

      {
        "userId": 1,
        "id": 1,
        "title": "A post's title",
        "body": "The post's content"
      }
      

      Since you’ll be reusing this pretty frequently, you can create a reusable component for this post. This can be done using the components object. You can define the post as a schema in the schemas object, which will be inside the components object. This schema is similar to the schema in a JSON Schema file.

      Add the post schema to your YAML file. Please note that the components object must be placed in the root of the document (without any indentation), not in the paths object.

      components:
        schemas:
          post:
            type: object
            properties:
              id:
                type: number
                description: ID of the post
              title:
                type: string
                description: Title of the post
              body:
                type: string
                description: Body of the post
              userId:
                type: number
                description: ID of the user who created the post
      

      The above schema is an object, denoted by type: object. It has four properties: id, title, body, and userId. That is how a schema is defined. Now you can use $ref in any object to reference this schema. This is defined as per the specification for URI syntax, RFC3986.

      Now that you have the schema, you can add the responses object. This object has items whose value is the status code returned, or default, to catch all other statuses, and the value is a response object. This object contains the description of the response, any headers that are returned, and the response body, along with the Content-Type in the content object.

      Add the responses object to the get operation of the /posts path by copying the highlighted lines:

      paths:
        "/posts":
          get:
            tags: ["posts"]
            summary: Returns all posts.
            responses:
              "200": # 200 Status Code
                description: All went well
                content:
                  application/json: # Reponse is returned in JSON
                    schema:
      

      Be sure to enclose 200 in quotes to make it a string and not a number.

      Here, you’re defining a response that gets returned with the 200 status code, and has a Content-Type header of application/json. In this schema object, you need to pass a reference to the post schema you just created. That can be done with $ref.

      Aside from schema, the application/json object can also contain any examples you wish to give.

      For now, add a reference to the post schema in schema.

      $ref: "#/components/schemas/post"
      

      # refers to the root of the document. Since the post schema is located in components/schemas/post, that’s how you should write it. And since # is a reserved symbol in YAML, you need to enclose the ref in quotes.

      Your Insomnia Design tab should look similar to this:

      Screenshot of Insomnia showing YAML in the center pane and a preview in the right pane.

      You can see that insomnia has rendered a preview of your document. The /posts route has been grouped into a posts section, because of the tag, and the correct response is also showing, as you defined in the schema. The same content-type you defined and the same schema you defined are previewed on the right.

      You can try changing something, like the tag or the responses of the path, and see it update in real time. Be sure to change it back after you’re done.

      Note: Press the Try It Out button in the Path operation in the preview, and then the Execute button to call the JSONPlaceholder API and receive a response.

      With the GET route documented, it’s time to document the POST /posts route. This will be quite similar to the previous operation, but this time, it will be a POST request, hence the object’s key is post (highlighted below). Add the following lines to your YAML file:

      paths:
        "/posts":
          # ...
          post:
            tags: ["posts"]
            summary: Create a new post
            responses:
              "200":
                description: A post was created
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/post"
      

      You’ve just defined another operation. This time, it is a POST request, with the same tag, so it gets grouped along with the GET request you defined earlier. The response also has the same schema since that is what will be returned by JSONPlaceHolder.

      There’s still one thing missing: the post method also accepts a request body. It hasn’t been documented yet, so add the requestBody object to the post operation. The request body is similar to the response object. There’s a description and content field, which are the same as the response object, and there’s also a required field, which is a boolean. This field governs whether a body is required for this request or not. In this case, it is, so add the requestBody object to your operation.

      paths:
        "/posts":
          # ...
          post:
            tags: ["posts"]
            summary: Create a new post
            requestBody:
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/post"
          required: true
            responses:
              "200":
                description: A post was created
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/post"
      

      At this point, your paths object should look like this:

      paths:
        "/posts":
          get:
            tags: ["posts"]
            summary: Returns all posts
            responses:
              "200":
                description: All went well
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/post"
          post:
            tags: ["posts"]
            summary: Create a new post
            requestBody:
              content:
                application/json:
                  schema:
                    $ref: "#/components/schemas/post"
              required: true
            responses:
              "200":
                description: A post was created
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/post"
      

      In this section, you documented the GET and POST operations available in the /posts route. Next, you’ll document the /posts/:id route, which is used to read, modify, or delete a single post.

      The /posts/:id Route

      Next, you’ll document the /posts/:id route. This route has three operations: GET, PUT, and DELETE. They get a single post, update a post, and delete a post. :id is a dynamic parameter that can be a number (for example: /posts/1, /posts/2, etc.). In OpenAPI, this is denoted as {id}, as shown in the following example:

      paths:
        "/posts":
        # ...
        "/posts/{id}":
        # TODO
      

      The in property defines where the parameter will be placed. This can be in the query string, in the cookie, in the header, or as a part of the path itself. The description is a description of the parameter. The required field is a boolean that indicates if the parameter is required. In the case of path parameters, required has to be true, since the parameter is a part of the path itself.

      Path parameters are special, so they’re defined in the path using braces ({}). The name of the parameter is enclosed in the braces and must match the name in the name field. First, you need to define id as a parameter object in the parameters array. Here’s how you’ll do it:

      paths:
        "/posts/{id}":
          parameters:
          - name: id # Must be same as the value in the {}.
            in: path
            description: ID of the post
            # Since this is in the path, the parameter HAS to be required
            required: true
            # Defining the type of the parameter
            schema:
              # In this case, it is just a string
              type: string
      

      Be sure to include the hyphen (-), otherwise the parameters array would become an object

      The last thing to document are the operations and their responses and request bodies, if they have any. This is similar to what you did in the previous section.

      First, add the GET /posts/:id operation, which gets a single post.

      get:
        tags: ["post"]
        summary: Get a single post
        responses:
          "200":
            description: All went well
            content:
              application/json:
                schema:
                  $ref: "#/components/schemas/post"
          "404":
            description: Post not found
            content:
              application/json:
                schema:
                  type: object
                  properties: {}
      

      Notice that this time, there is a 404 response. This is because the GET request can return a 404 error if the post is not found. The properties: {} in the above code is how you’d define an empty object in YAML.

      Next, add the PUT /posts/:id operation, which updates a post. This method combines the GET and POST methods above, since it has both a requestBody and a 404 response.

      put:
        tags: ["post"]
        summary: Update a post
        requestBody:
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/post"
          required: true
        responses:
          "200":
            description: All went well
            content:
              application/json:
                schema:
                  $ref: "#/components/schemas/post"
          "404":
            description: Post not found
            content:
              application/json:
                schema:
                  type: object
                  properties: {}
      

      JSONPlaceholder doesn’t really validate the data you send it, so there is no 400 or 422 response, but if the API you’re documenting does something like that (which it should), be sure to document those responses as well. To avoid repeating yourself, you can create response components, as you did in the previous section.

      And finally, add the DELETE /posts/:id operation, which deletes a post. This is the same as the GET method, since it returns a 404, but this time, the operation is delete.

      delete:
        tags: ["post"]
        summary: Delete a post
        responses:
          "200":
            description: All went well
            content:
              application/json:
                schema:
                  type: object
                  properties: {}
          "404":
            description: Post not found
            content:
              application/json:
                schema:
                  type: object
                  properties: {}
      

      Note that the DELETE method only returns an empty object ({}), even on a 200 response.

      And with that, you’ve successfully documented the /posts route of JSONPlaceholder. Here’s the full YAML document.

      openapi: 3.0.3
      
      info:
        title: JSONPlaceholder
        description: Free fake API for testing and prototyping.
        version: 0.1.0
      
      externalDocs:
        description: "JSONPlaceholder's guide"
        url: https://jsonplaceholder.typicode.com/guide
      
      servers:
      - url: https://jsonplaceholder.typicode.com
        description: JSONPlaceholder
      
      paths:
        "/posts":
          get:
            tags: ["posts"]
            summary: Returns all posts
            responses:
              "200":
                description: All went well
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/post"
          post:
            tags: ["posts"]
            summary: Create a new post
            requestBody:
              content:
                application/json:
                  schema:
                    $ref: "#/components/schemas/post"
              required: true
            responses:
              "200":
                description: A post was created
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/post"
        "/posts/{id}":
          parameters:
          - name: id # Must be same as the value in the {}.
            # Location of the parameter.
            # Can be `path`, `cookie`, `query` or `header`
            in: path
            description: ID of the post
            # Since this is in the path, the parameter HAS to be required
            required: true
            # Defining the type of the parameter
            schema:
              # In this case, it is just a string
              type: string
          get:
            tags: ["post"]
            summary: Get a single post
            responses:
              "200":
                description: All went well
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/post"
              # But this time, you can also get a 404 response,
              # which is an empty JSON object.
              "404":
                description: Post not found
                content:
                  application/json:
                    schema:
                      type: object
                      properties: {}
          put:
            tags: ["post"]
            summary: Update a post
            requestBody:
              content:
                application/json:
                  schema:
                    $ref: "#/components/schemas/post"
              required: true
            responses:
              "200":
                description: All went well
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/post"
              "404":
                description: Post not found
                content:
                  application/json:
                    schema:
                      type: object
                      properties: {}
          delete:
            tags: ["post"]
            summary: Delete a post
            responses:
              "200":
                description: All went well
                content:
                  application/json:
                    schema:
                      type: object
                      properties: {}
              # But this time, you can also get a 404 response,
              # which is an empty JSON object.
              "404":
                description: Post not found
                content:
                  application/json:
                    schema:
                      type: object
                      properties: {}
      
      components:
        schemas:
          post:
            type: object
            properties:
              id:
                type: number
                description: ID of the post
              title:
                type: string
                description: Title of the post
              body:
                type: string
                description: Body of the post
              userId:
                type: number
                description: ID of the user who created the post
      

      In the above document, you’ve documented all /posts routes provided by JSONPlaceholder, and you’ve also covered all HTTP methods that are supported. you’ve also learned about parameters, request bodies, and different responses.

      With the OpenAPI document complete, the next step is to make it available to users.

      Step 8 — Using Redoc to Display API Documentation

      While Insomnia does have a nice-looking Preview pane, you can’t expect all of your users to have Insomnia installed, so you’ll use Redoc to display the OpenAPI YAML file in a nice readable way.

      To build Redoc, you need to have NodeJS installed. (Please note that you don’t need to know any NodeJS or JavaScript to build Redoc.)

      Create a new folder anywhere on your computer. You’ll be building Redoc in this folder and deploy it to GitHub.

      First, you’ll need to save your current OpenAPI document to this folder. Create a new file called openapi.yaml in the current folder and copy-paste the contents in Insomnia’s Design tab to this file. Redoc can now use this file to generate your API documentation

      Next, open a terminal in that folder and run the below command to build Redoc.

      • npx redoc-cli --output index.html bundle openapi.yaml

      npx is the NPM (Node Package Manager)’s CLI tool to fetch a CLI-installable package and run it. This allows you to run redoc-cli without actually installing it to your global $PATH. Instead, it will be available via npx. Be sure to type y if asked to install redoc-cli or not. Next, you’re telling Redoc to bundle the openapi.yaml file you just created into a zero-dependency HTML file, which in this case, will be index.html, since you passed the --output flag.

      This should create a new file called index.html in that directory. This file is the documentation, powered by Redoc. Open the file in your browser and inspect it to make sure that every route you’ve defined is covered.

      Screenshot of the Redoc documentation displayed in a browser.

      Now that you have your documentation site generated, it’s time to make it available to others using GitHub Pages.

      Step 9 — Deploying to GitHub Pages

      Now that you have a documentation website, you can use GitHub Pages to deploy it for the world to see. As part of the prerequisites, you created a new GitHub Repository. Copy the clone URL shown in Quick Setup. You’ll use the git command to push your openapi.yaml and index.html files to GitHub. Be sure to run the below commands in the folder that contains these two files.

      First, you’ll initialize the git repository and commit all of your files:

      • git init
      • git add .
      • git commit -m "First commit" # Feel free to change this message

      Next, you’ll deploy your changes to GitHub. First, you need to tell git that your GitHub repository should be the remote repository. The remote repository is usually stored under the name origin.

      • git remote add origin YOUR_GITHUB_REPO_URL

      And finally, push your changes to GitHub with this command:

      Note: Git has changed the name of the default branch from master to main, so main is used in the command above. Feel free to replace main with master if you like.

      Refresh GitHub, and you should see your two files there.

      Screenshot of two files on GitHub

      Now you need to enable GitHub Pages. Go to your repository’s settings and click the Pages button on the Sidebar. Change the source branch to your default branch (usually main or master) and the folder to / (root) and click Save.

      Finally, visit https://your_username.github.io/your_repo_name, you should see the Redoc page you’ve just created. You can view my version published to GitHub Pages here.

      With that, your API is now available for anyone to see with the URL above.

      Conclusion

      In this tutorial, you documented the /posts route of the JSONPlaceholder API. You documented path parameters as well as request bodies and possible responses. You have also learned to reduce boilerplate using reusable components. Feel free to check out the source code and the live version.

      As a next step, try to document the other routes that JSONPlaceholder offers (e.g., /users), or try this out with your own API. You can go forward from here by using tools like Docasaurus to add documentation that explains and guides the user. Remember to keep your API Spec DRY and easy to read so you can make changes to it in the future. Insomnia also has other features, like the ability to test and debug your API, so be sure to check those out by reading the documentation.



      Source link

      How To Build a Python REST API with Fauna and Deploy it to DigitalOcean App Platform


      Introduction

      Many developers don’t have the time or experience to set up and manage infrastructure for their applications. To keep up with deadlines and reduce costs, developers need to find solutions that allow them to deploy their apps to the cloud as quickly and efficiently as possible to focus on writing the code and delivering new features to their customers. Together, DigitalOcean’s App Platform and Fauna provide that ability.

      DigitalOcean App Platform is a Platform-as-a-Service (PaaS) that abstracts the infrastructure that runs your apps. It also lets you deploy applications by pushing your code to a Git branch.

      Fauna is a powerful data layer for applications of any size. As you’ll see in this tutorial, with Fauna, you can get a database up and running quickly without having to worry about the database operations.

      Together, these two solutions let you focus on your application instead of managing your infrastructure.

      In this tutorial, you’ll integrate Fauna with Python by writing a minimal REST API using the Flask framework. You’ll then deploy the API to DigitalOcean’s App Platform from a Git repository.The API will consist of:

      • A public /signup POST endpoint for creating users in the Users collection.
      • A public /login POST endpoint for authenticating with the documents in the Users collection.
      • A private /things GET endpoint for fetching a list of Fauna documents from the Things collection.

      The finished Python project is available at this Github repository.

      Prerequisites

      Before starting this tutorial, you will need:

      Step 1 — Setting Up the Fauna Database

      In the first step, you will configure a Fauna database and create the collections for the API. Fauna is a document-based database rather than a traditional table-based relational database. Fauna stores your data in documents and collections, which are groups of documents.

      To create the collections, you will execute queries using FQL, Fauna’s native query language. FQL is an expressive and powerful query language that gives you access to the full power of Fauna.

      To get started, log into Fauna’s dashboard. After logging in, click on the Create Database button at the top.

      In the New Database form, use PYTHON_API for the database name:

      Creating a Fauna database

      Leave Pre-populate with demo data unchecked. Press the Save button.

      After creating the database, you will see the home section for your database:

      Fauna database home

      You’re now going to create two collections:

      • The Users collection that will store documents with authentication information.
      • The Things collection to store some mock data to test your API.

      To create these collections, you’ll execute some FQL queries in the dashboard’s shell. Access the shell from the main dashboard menu on the left:

      Fauna dashboard shell

      Write the following FQL query in the bottom panel of the shell to create a collection called Things by using the CreateCollection function:

      CreateCollection({name: "Things"})
      

      Press the RUN QUERY button. You will get a result similar to this in the shell’s top panel:

      {
        ref: Collection("Things"),
        ts: 1614805457170000,
        history_days: 30,
        name: "Things"
      }
      

      The result shows four fields:

      • ref is a reference to the collection itself.
      • ts is the timestamp of its creation in microseconds.
      • history_days is how long Fauna will retain changes on documents’ changes.
      • name is the collection name.

      Next, create the Users collection with the following query:

      CreateCollection({name: "Users"})
      

      Now that both collections are in place, you will create your first document.

      Documents in Fauna are somewhat similar to JSON objects. Documents can store strings, numbers, and arrays, but they can also use Fauna data types. A common Fauna type is Ref, which represents a reference to a document in a collection.

      The Create function creates a new document into the specified collection. Run the following query to create a document in the Things collection with two fields:

      Create(
        Collection("Things"),
        {
          data: {
            name: "Banana",
            color: "Yellow"
          }
        }
      )
      

      After running that query, Fauna will return the created document:

      {
        ref: Ref(Collection("Things"), "292079274901373446"),
        ts: 1614807352895000,
        data: {
          name: "Banana",
          color: "Yellow"
        }
      }
      

      The result shows the following fields:

      • ref of type Ref is a reference to this document in the Things collection with the ID 292079274901373446. Do note that your document will have a different ID.
      • ts is the timestamp of its creation in microseconds.
      • data is the actual content of the document.

      This result looks similar to the result you got when you created a collection. That’s because all entities in Fauna (collections, indexes, roles, etc) are actually stored as documents.

      To read documents, use the Get function which accepts a reference of a document. Run the Get query using the reference for your document:

      Get(Ref(Collection("Things"), "292079274901373446"))
      

      The result is the full document:

      {
        ref: Ref(Collection("Things"), "292079274901373446"),
        ts: 1614807352895000,
        data: {
          name: "Banana",
          color: "Yellow"
        }
      }
      

      To get all references for documents stored in a collection, use the Documents function with the Paginate function:

      Paginate(Documents(Collection("Things")))
      

      This query returns a page with an array of references:

      {
        data: [Ref(Collection("Things"), "292079274901373446")]
      }
      

      To get actual documents instead of references, iterate over the references using Map. Then use a Lambda (an anonymous function) to iterate over the array of references and Get each reference:

      Map(
        Paginate(Documents(Collection("Things"))),
        Lambda("ref", Get(Var("ref")))
      )
      

      The result is an array containing full documents:

      {
        data: [
          {
            ref: Ref(Collection("Things"), "292079274901373446"),
            ts: 1614807352895000,
            data: {
              name: "Banana",
              color: "Yellow"
            }
          }
        ]
      }
      

      You’re now going to create the Users_by_username index. You typically use indexes in Fauna to catalog, filter, and sort data, but you can also use them for other purposes like enforcing unique constraints.

      The Users_by_username index will find users by their username, and also enforce a unique constraint to prevent two documents from having the same username.

      Execute this code in the shell to create the index:

      CreateIndex({
        name: "Users_by_username",
        source: Collection("Users"),
        terms: [{ field: ["data", "username"] }],
        unique: true
      })
      

      The CreateIndex function will create an index with the configured settings:

      • name is the name of the index.
      • source is the collection (or collections) the index will index data from.
      • terms is the search/filter terms you’ll pass to this index when using it to find documents.
      • unique means that the indexed values will be unique. In this example, the username property of the documents in the Users collection will be enforced as unique.

      To test the index, create a new document inside the Users collection by running the following code in the Fauna shell:

      Create(
        Collection("Users"),
        {
          data: {
            username: "sammy"
          }
        }
      )
      

      You’ll see a result like the following:

      {
        ref: Ref(Collection("Users"), "292085174927098368"),
        ts: 1614812979580000,
        data: {
          username: "sammy"
        }
      }
      

      Now try to create a document with the same username value:

      Create(
        Collection("Users"),
        {
          data: {
            username: "sammy"
          }
        }
      )
      

      You’ll receive an error now:

      Error: [
        {
          "position": [
            "create"
          ],
          "code": "instance not unique",
          "description": "document is not unique."
        }
      ]
      

      Now that the index is in place, you can query it and fetch a single document. Run this code in the shell to fetch the sammy user using the index:

      Get(
        Match(
          Index("Users_by_username"),
          "sammy"
        )
      )
      

      Here’s how it works:

      • Index returns a reference to the Users_by_username index.
      • Match returns a reference to the matched document (the one that has a username with the value of sammy).
      • Get takes the reference returned by Match, and fetches the actual document.

      The result of this query will be:

      {
        ref: Ref(Collection("Users"), "292085174927098368"),
        ts: 1614812979580000,
        data: {
          username: "sammy"
        }
      }
      

      Delete this testing document by passing its reference to the Delete function:

      Delete(Ref(Collection("Users"), "292085174927098368"))
      

      Next you’ll configure security settings for Fauna so you can connect to it from your code.

      Step 2 — Configuring a Server Key and Authorization Rules

      In this step you’ll create a server key that your Python application will use to communicate with Fauna. Then you’ll configure access permissions.

      To create a key, go to the Security section of the Fauna dashboard by using the main menu on the left. Once there:

      1. Press the New Key button.
      2. Select the Server role.
      3. Press Save.

      Creating a Fauna key

      After saving, the dashboard will show you the key’s secret. Save the secret somewhere safe and never commit it to your Git repository.

      Warning: The Server role is omnipotent and anyone with this secret would have full access to your database. As its name implies, this is the role typically used by trusted server applications, although it is also possible to create a key with a custom role with limited privileges. When you create production applications, you’ll want to make a more restrictive role.

      By default, everything in Fauna is private, so you’re now going to create a new role to allow the logged-in users to read documents from the Things collection.

      In the Security section of the dashboard, go to Roles, and create a new custom role with the name User.

      In the Collections dropdown, add the Things collection and press the Read permission so that it shows a green check mark:

      Configuring the permissions of a Fauna role

      Before saving the role, go to the Membership tab and add the Users collection to the role:

      Configuring the memerbship of a Fauna role

      You can now save your User custom role by pressingthe Save button.

      Now any logged-in user from a document in the Users collection will be able to read any document from the Things collection.

      With authentication and authorization in place, let’s now create the Python API that will talk to Fauna.

      Step 3 — Building the Python Application

      In this step you will build a small REST API using the Flask framework, and you’ll write FQL queries in Python, connecting to your Fauna database using the the Fauna driver.

      To get started, create a project folder and access it from your terminal.

      First install Flask with:

      Then install the Fauna Python driver with:

      In your project folder, create the file main.py and add the following code to the file, which adds the necessary imports, the FAUNA_SECRET environment variable, and the basic configuration of the Flask application:

      main.py

      import os
      FAUNA_SECRET = os.environ.get('FAUNA_SECRET')
      
      import flask
      from flask import request
      
      import faunadb
      from faunadb import query as q
      from faunadb.client import FaunaClient
      
      app = flask.Flask(__name__)
      app.config["DEBUG"] = True
      

      The FAUNA_SECRET environment variable will carry the server secret you created earlier. To be able to run this application, locally or in the cloud, this variable needs to be injected. Without it, the application won’t be able to connect to Fauna. You’ll provide this environment variable when you launch the app.

      Now add the the /signup route to the main.py file. This will create new documents in the Users collection:

      main.py

      @app.route('/signup', methods=['POST'])
      def signup():
      
          body = request.json
          client = FaunaClient(secret=FAUNA_SECRET)
      
          try:
              result = client.query(
                  q.create(
                      q.collection("Users"),
                      {
                          "data": {
                              "username": body["username"]
                          },
                          "credentials": {
                              "password": body["password"]
                          }
                      }
                  )
              )
      
              return {
                  "userId": result['ref'].id()
              }
      
          except faunadb.errors.BadRequest as exception:
              error = exception.errors[0]
              return {
                  "code": error.code,
                  "description": error.description
              }, 409
      

      Note that the Fauna client is being instantiated on every request using the server secret:

      main.py

      ...
      client = FaunaClient(secret=FAUNA_SECRET)
      ...
      

      Once users are logged in, the API will execute queries on behalf of each user using different secrets, which is why it makes sense to instantiate the client on every request.

      Unlike other databases, the Fauna client does not maintain a persistent connection. From the outside world, Fauna behaves like an API; every query is a single HTTP request.

      After the client is ready, the FQL query executes, which creates a new document in the Users collection. Each Fauna driver translates idiomatic syntax to FQL statements. In this route, you added this query:

      main.py

      ...
      q.create(
          q.collection("Users"),
          {
              "data": {
                  "user": json["user"]
              },
              "credentials": {
                  "password": json["password"]
              }
          }
      )
      ...
      

      This is what this query would look like in native FQL:

      Create(
          Collection("Users"),
          {
              "data": {
                  "user": "sammy"
              },
              "credentials": {
                  "password": "secretpassword"
              }
          }
      )
      

      In addition to the document data, you’re adding a credentials configuration with the user’s password. This part of the document is completely private. You will never be able to read a document’s credentials afterwards. When using Fauna’s authentication system, it’s not possible to expose users’ passwords by mistake.

      Finally, if there’s already a user with the same username, a faunadb.errors.BadRequest exception will be raised, and a 409 response with the error information will be returned to the client.

      Next, add the /login route in the main.py file to authenticate the user and password. This follows a similar pattern as the previous example; you execute a query using the Fauna connection and if the authentication fails, you raise a faunadb.errors.BadRequest exception and return a a 401 response with the error information. Add this code to main.py:

      main.py

      @app.route('/login', methods=['POST'])
      def login():
      
          body = request.json
          client = FaunaClient(secret=FAUNA_SECRET)
      
          try:
              result = client.query(
                  q.login(
                      q.match(
                          q.index("Users_by_username"),
                          body["username"]
                      ),
                      {"password": body["password"]}
                  )
              )
      
              return {
                  "secret": result['secret']
              }
      
          except faunadb.errors.BadRequest as exception:
              error = exception.errors[0]
              return {
                  "code": error.code,
                  "description": error.description
              }, 401
      

      This is the FQL query used to authenticate users with Fauna:

      main.py

      q.login(
          q.match(
              q.index("Users_by_username"),
              body["username"]
          ),
          {"password": body["password"]}
      )
      

      This is what this query would look like in native FQL:

      Login(
          Match(
              Index("Users_by_username"),
              "sammy"
          ),
          {"password": "secretpassword"}
      )
      

      Match returns a reference to a document using the Users_by_username index that we created previously.

      If the provided password matches the referenced document, Login will create a new token and return a dictionary with the following keys:

      • ref with a reference to the token for the new document.
      • ts with the timestamp of the transaction.
      • instance with a reference to the document that was used to do the authentication.
      • secret with the token’s secret that will be used to make further queries to Fauna.

      If you run that FQL query into your Fauna dashboard’s shell you will see something similar to this:

      {
        ref: Ref(Ref("tokens"), "292001047221633538"),
        ts: 1614732749110000,
        instance: Ref(Collection("Users"), "291901454585692675"),
        secret: "fnEEDWVnxbACAgQNBIxMIAIIKq1E5xvPPdGwQ_zUFH4F5Dl0neg"
      }
      

      Depending on the security requirements of the project, you have to decide how to handle the token’s secret. If this API was meant to be consumed by browsers, you might return the secret inside a secure cookie or an encrypted JSON Web Token (JWT). Or you might store it as session data somewhere else, like a Redis instance. For the purpose of this demo, you return it in the body of the HTTP response:

      Finally, add this bit of code to main.py, which will start the Flask application:

      main.py

      app.run(host=os.getenv('IP', '0.0.0.0'), port=int(os.getenv('PORT', 8080)))
      

      It’s important to specify the 0.0.0.0 address. Once deployed to the cloud, this application will run in a Docker container. It won’t be able to receive requests from remote clients if it is running on 127.0.0.1, which is the default address for Flask applications.

      This is the complete main.py file so far:

      main.py

      import os
      FAUNA_SECRET = os.environ.get('FAUNA_SECRET')
      
      import flask
      from flask import request
      
      import faunadb
      from faunadb import query as q
      from faunadb.client import FaunaClient
      
      app = flask.Flask(__name__)
      app.config["DEBUG"] = True
      
      @app.route('/signup', methods=['POST'])
      def signup():
      
          body = request.json
          client = FaunaClient(secret=FAUNA_SECRET)
      
          try:
              result = client.query(
                  q.create(
                      q.collection("Users"),
                      {
                          "data": {
                              "username": body["username"]
                          },
                          "credentials": {
                              "password": body["password"]
                          }
                      }
                  )
              )
      
              return {
                  "userId": result['ref'].id()
              }
      
          except faunadb.errors.BadRequest as exception:
              error = exception.errors[0]
              return {
                  "code": error.code,
                  "description": error.description
              }, 409
      
      @app.route('/login', methods=['POST'])
      def login():
      
          body = request.json
          client = FaunaClient(secret=FAUNA_SECRET)
      
          try:
              result = client.query(
                  q.login(
                      q.match(
                          q.index("Users_by_username"),
                          body["username"]
                      ),
                      {"password": body["password"]}
                  )
              )
      
              return {
                  "secret": result['secret']
              }
      
          except faunadb.errors.BadRequest as exception:
              error = exception.errors[0]
              return {
                  "code": error.code,
                  "description": error.description
              }, 401
      
      app.run(host=os.getenv('IP', '0.0.0.0'), port=int(os.getenv('PORT', 8080)))
      

      Save the file.

      To launch this server locally from your terminal, use the following command with the FAUNA_SECRET environment variable with the secret you obtained when creating the server key:

      • FAUNA_SECRET=your_fauna_server_secret python main.py

      After triggering that command, Flask will show a warning informing you it is running with a development WSGI server. This is fine for the purpose of this demo so you can safely ignore this warning.

      Test your API by making HTTP requests using the curl command. Open a new terminal window and run the following command:

      Create a user with the following command:

      • curl -i -d '{"user":"sammy", "password": "secretpassword"}' -H 'Content-Type: application/json' -X POST http://0.0.0.0:8080/signup

      You’ll see the following response, indicating a successful user creation:

      HTTP/1.0 200 OK
      Content-Type: application/json
      Content-Length: 37
      Server: Werkzeug/1.0.1 Python/3.9.2
      Date: Thu, 04 Mar 2021 01:00:47 GMT
      
      {
        "userId": "292092166117786112"
      }
      

      Now authenticate that user with this command:

      • curl -i -d '{"user":"sammy", "password": "secretpassword"}' -H 'Content-Type: application/json' -X POST http://0.0.0.0:8080/login

      You’ll get this successful response:

      HTTP/1.0 200 OK
      Content-Type: application/json
      Content-Length: 70
      Server: Werkzeug/1.0.1 Python/3.9.2
      Date: Thu, 04 Mar 2021 01:01:19 GMT
      
      {
        "secret": "fnEEDbhO3jACAAQNBIxMIAIIOlDxujk-VJShnnhkZkCUPKIHxbc"
      }
      

      Close the terminal window where you ran your curl commands and switch back to the terminal where your Python server is running. Stop your server by pressing CTRL+C.

      Now that the application is working, we’re going to add a private endpoint that requires users to be authenticated.

      Step 4 — Adding a Private Endpoint

      In this step, you’ll add a private endpoint to the API, which will require the user to be authenticated first.

      First, create a new route in the main.py file. This route will respond to the /things endpoint. Place it above the line that starts the server with the app.run() method:

      main.py

      @app.route('/things', methods=['GET'])
      def things():
      

      Next, in the /things route, instantiate the Fauna client:

      main.py

          userSecret = request.headers.get('fauna-user-secret')
          client = FaunaClient(secret=userSecret)
      

      Instead of using the server secret, this route is using the user’s secret from the fauna-user-secret HTTP header which is used to instantiate the Fauna client. By using the users’ secrets instead of the server secret, FQL queries will now be subject to the authorization rules we’ve configured previously in the dashboard.

      Then add this try block to the route to execute the query:

      main.py

          try:
              result = client.query(
                  q.map_(
                      q.lambda_("ref", q.get(q.var("ref"))),
                      q.paginate(q.documents(q.collection("Things")))
                  )
              )
      
              things = map(
                  lambda doc: {
                      "id": doc["ref"].id(),
                      "name": doc["data"]["name"],
                      "color": doc["data"]["color"]
                  },
                  result["data"]
              )
      
              return {
                  "things": list(things)
              }
      

      This executes an FQL query and parses the Fauna response into a serializable type that is then returned as a JSON string in the body of the HTTP response.

      Finally, add this except block to the route:

      main.py

          except faunadb.errors.Unauthorized as exception:
              error = exception.errors[0]
              return {
                  "code": error.code,
                  "description": error.description
              }, 401
      

      If the request doesn’t contain a valid secret, a faunadb.errors.Unauthorized exception will be raised and a 401 response with the error information will be returned.

      This is the full code for the /things route:

      main.py

      @app.route('/things', methods=['GET'])
      def things():
      
          userSecret = request.headers.get('fauna-user-secret')
          client = FaunaClient(secret=userSecret)
      
          try:
              result = client.query(
                  q.map_(
                      q.lambda_("ref", q.get(q.var("ref"))),
                      q.paginate(q.documents(q.collection("Things")))
                  )
              )
      
              things = map(
                  lambda doc: {
                      "id": doc["ref"].id(),
                      "name": doc["data"]["name"],
                      "color": doc["data"]["color"]
                  },
                  result["data"]
              )
      
              return {
                  "things": list(things)
              }
      
          except faunadb.errors.Unauthorized as exception:
              error = exception.errors[0]
              return {
                  "code": error.code,
                  "description": error.description
              }, 401
      

      Save the file and run your server again:

      • FAUNA_SECRET=your_fauna_server_secret python main.py

      To test this endpoint, first obtain a secret by authenticating with valid credentials. Open a new terminal window and execute this curl command:

      • curl -i -d '{"username":"sammy", "password": "secretpassword"}' -H 'Content-Type: application/json' -X POST http://0.0.0.0:8080/login

      This command returns a succesful response, although the value for secret will be different:

      HTTP/1.0 200 OK
      Content-Type: application/json
      Content-Length: 70
      Server: Werkzeug/1.0.1 Python/3.9.2
      Date: Thu, 04 Mar 2021 01:01:19 GMT
      
      {
        "secret": "fnEEDb...."
      }
      

      Now hen do a GET request to /things using the secret:

      curl -i -H 'fauna-user-secret: fnEEDb...' -X GET http://0.0.0.0:8080/things
      

      You’ll get another successful response:

      HTTP/1.0 200 OK
      Content-Type: application/json
      Content-Length: 118
      Server: Werkzeug/1.0.1 Python/3.9.2
      Date: Thu, 04 Mar 2021 01:14:49 GMT
      
      {
        "things": [
          {
            "color": "Yellow",
            "id": "292079274901373446",
            "name": "Banana"
          }
        ]
      }
      

      Close your terminal window where you ran the curl commands. Return to your window where your server is running and stop the server with CTRL+C.

      Now that you have a working app, you’re ready to deploy it.

      Step 4 — Deploying to DigitalOcean

      In the final step of this tutorial, you will create an app on App Platform and deploy it from a GitHub repository.

      Before pushing the project to a Git repository, be sure to run the following command in the project’s folder:

      • pip freeze > requirements.txt

      This will create a requirements.txt file with the list of dependencies that need to be installed once the application is deployed.

      Now initialize your project directory as a Git repository:

      Now execute the following command to add files to your repository:

      This adds all the files in the current directory.

      With the files added, make your initial commit:

      • git commit -m "Initial version of the site"

      Your files will commit.

      Open your browser and navigate to GitHub, log in with your profile, and create a new repository called sharkopedia. Create an empty repository without a README or license file.

      Once you’ve created the repository, return to the command line to push your local files to GitHub.

      First, add GitHub as a remote repository:

      • git remote add origin https://github.com/your_username/sharkopedia

      Next, rename the default branch main, to match what GitHub expects:

      Finally, push your main branch to GitHub’s main branch:

      Your files will transfer. You’re now ready to deploy your app.

      Note: To be able to create an app on App Platform, you’ll first need to add a payment method to your DigitalOcean account.

      The application will run on a container which costs $5 per month, although only a few cents will be needed to test it out. Don’t forget to delete the application once you’re done or you’ll continue to be charged.

      Go to the Apps section of the DigitalOcean dashboard, and click on Launch Your App:

      Select the source for deployment. You will need to authorize DigitalOcean to read your Github repositories. Once you’ve authorized access, select the repository with your Python project and the branch that contains the version of the app you want to deploy:

      Selecting a repository and branch

      At this point, App Platform will determine that your project uses Python and will let you configure some application options:

      Configuring the app options

      Set the following options

      • Ensure the Type is Web Service.
      • Create aFAUNA_SECRET environment variable with your server secret.
      • Set the Run Command to python main.py.
      • Set the HTTP Port to 8080.

      Next, enter a name for your app and select a deploy region:

      Configuring the name of the app and deploy region

      Next, choose the Basic plan and Basic Size that costs $5 per month:

      Selecting the app plan

      After that, scroll down and click on Launch Your App.

      Once you’ve finished configuring the app, a container will be created and deployed with your application. This first-time initialization will take a couple of minutes, but subsequent deploys will be much faster.

      In the app’s dashboard you’ll see a green check mark to indicate the deploy process has finished successfully:

      App is running

      You will now be able to execute HTTP requests to the provided app domain. Execute the following command in your terminal, substituting your_app_name with your actual app name, to return a new secret for the sammy user:

      • curl -i -d '{"user":"sammy", "password": "secretpassword"}' -H 'Content-Type: application/json' -X POST https://your_app_name.ondigitalocean.app/login

      You’ll receive a response similar to the following:

      HTTP/1.0 200 OK
      Content-Type: application/json
      Content-Length: 70
      Server: Werkzeug/1.0.1 Python/3.9.2
      Date: Thu, 04 Mar 2021 01:01:19 GMT
      
      {
        "secret": "fnAADbhO3jACEEQNBIxMIAOOIlDxujk-VJShnnhkZkCUPKIskdjfh"
      }
      

      Your application is now up and running on Digital Ocean.

      Conclusion

      In this tutorial you created a Python REST API using Fauna as the data layer, and you deployed it to DigitalOcean App Platform.

      To keep learning about Fauna and dive deeper into FQL, check out the Fauna Documentation.



      Source link