One place for hosting & domains

      API

      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).

      Method Route Description
      GET /posts Get all posts
      GET /posts/:id Get a single post
      POST /posts Create a post
      PUT/PATCH /posts/:id Update a post
      DELETE /posts/:id Delete 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:

      Name Type Description
      openapi string REQUIRED. Version of the OpenAPI schema.
      info Info Object REQUIRED. An object containing information about the API.
      servers Array of Server Objects An array containing objects that provide connectivity options to an API server.
      paths Paths objects REQUIRED. An object containing the routes provided by the API, methods, request-bodies, parameters and responses. This is the most important part of the document.
      components Components Object Contains reusable components, meant to reduce file size and keep the docs clean.
      security Array of Security Objects Contains a list of authentication mechanisms for the API. Outside the scope of this tutorial.
      externalDocs External Documentation Object Contains 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:

      Name Type Description
      title string REQUIRED. The title of the API.
      description string A short description of the API. Markdown can be used here.
      termsOfService string A URL to the Terms of Service for the API.
      contact Contact Object The contact information for the exposed API.
      license License Object The license information for the exposed API.
      version string REQUIRED. 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 Name Type Description
      description string A short description of the target documentation. Markdown can be used.
      url string REQUIRED. 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:

      Name Type Description
      summary string An optional summary of this route.
      description string An optional description of what the route can do.
      get/post/put/patch/delete/etc Operation Object A definition of an operation (method) on this route.
      servers Array of Server Objects An alternative server array to service all operations in this path.
      parameters An array of Parameter Object Parameters 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:

      Name Type Description
      tags Array of strings A list of tags for API documentation control. Tags can be used for grouping similar routes.
      summary string A short summary of what the operation does.
      description string A description of the operation. Markdown can be used here.
      externalDocs External Documentation Object Additional external documentation for this operation. Same as externalDocs on the main object.
      parameters Array of Parameter Objects Same as parameters in the Path Item object.
      requestBody Request Body Object The body of the request. This can NOT be used when the method GET or DELETE.
      responses Responses Object REQUIRED. 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 GraphQL API With Golang to Upload Files to DigitalOcean Spaces


      The author selected the Diversity in Tech Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      For many applications, one desirable feature is the user’s ability to upload a profile image. However, building this feature can be a challenge for developers new to GraphQL, which has no built-in support for file uploads.

      In this tutorial, you will learn to upload images to a third-party storage service directly from your backend application. You will build a GraphQL API that uses an S3-compatible AWS GO SDK from a Go backend application to upload images to DigitalOcean Spaces, which is a highly scalable object storage service. The Go back-end application will expose a GraphQL API and store user data in a PotsgreSQL database provided by DigitalOcean’s Managed Databases service.

      By the end of this tutorial, you will have built a GraphQL API using Golang that can receive a media file from a multipart HTTP request and upload the file to a bucket within DigitalOcean Spaces.

      Prerequisites

      To follow this tutorial, you will need:

      • A DigitalOcean account. If you do not have one, sign up for a new account. You will use DigitalOcean’s Spaces and Managed Databases in this tutorial.

      • A DigitalOcean Space with Access Key and Access Secret, which you can create by following the tutorial, How To Create A DigitalOcean Space and API Key. You can also see product documentation for How to Manage Administrative Access to Spaces.

      • Go installed on your local machine, which you can do by following our series, How to Install and Set Up a Local Programming Environment for Go. This tutorial used Go version 1.17.1.

      • Basic knowledge of Golang, which you can gain from our How To Code in Go series. The tutorial, How To Write Your First Program In Go, provides a good introduction to the Golang programming language.

      • An understanding of GraphQL, which you can find in our tutorial, An Introduction To GraphQL.

      Step 1 — Bootstrapping a Golang GraphQL API

      In this step, you will use the Gqlgen library to bootstrap the GraphQL API. Gqlgen is a Go library for building GraphQL APIs. Two important features that Gqglen provides are a schema-first approach and code generation. With a schema-first approach, you first define the data model for the API using the GraphQL Schema Definition Language (SDL). Then you generate the boilerplate code for the API from the defined schema. Using the code generation feature, you do not need to manually create the query and mutation resolvers for the API as they are automatically generated.

      To get started, execute the command below to install gqlgen:

      • go install github.com/99designs/gqlgen@latest

      Next, create a project directory named digitalocean to store the files for this project:

      Change into the digitalocean project directory:

      From your project directory, run the following command to create a go.mod file that manages the modules within the digitalocean project:

      Next, using nano or your favorite text editor, create a file named tools.go within the project directory:

      Add the following lines into the tools.go file as a tool for the project:

      // +build tools
      
       package tools
      
       import _ "github.com/99designs/gqlgen" 
      

      Next, execute the tidy command to install the gqlgen dependency introduced within the tools.go file:

      Finally, using the installed Gqlgen library, generate the boilerplate files needed for the GraphQL API:

      Running the gqlgen command above generates a server.go file for running the GraphQL server and a graph directory containing a schema.graphqls file that contains the Schema Definitions for the GraphQL API.

      In this step, you used the Gqlgen library to bootstrap the GraphQL API. Next, you’ll define the schema of the GraphQL application.

      Step 2 — Defining the GraphQL Application Schema

      In this step, you will define the schema of the GraphQL application by modifying the schema.graphqls file that was automatically generated when you ran the gqlgen init command. In this file, you will define a User, Query, and Mutation types.

      Navigate to the graph directory and open the schema.graphqls file, which defines the schema of the GraphQL application. Replace the boilerplate schema with the following code block, which defines the User type with a Query to retrieve all user data and a Mutation to insert data:

      schema.graphqls

      
      scalar Upload
      
      type User {
        id: ID!
        fullName: String!
        email: String!
        img_uri: String!
        DateCreated: String!
      }
      
      type Query {
        users: [User]!
      }
      
      input NewUser {
        fullName: String!
        email: String!
        img_uri: String
        DateCreated: String
      }
      
      input ProfileImage {
        userId: String
        file: Upload
      }
      
      type Mutation {
        createUser(input: NewUser!): User!
        uploadProfileImage(input: ProfileImage!): Boolean!
      }
      

      The code block defines two Mutation types and a single Query type for retrieving all users. A mutation is used to insert or mutate existing data in a GraphQL application, while a query is used to fetch data, similar to the GET HTTP verb in a REST API.

      The schema in the code block above used the GraphQL Schema Definition Language to define a Mutation containing the CreateUser type, which accepts the NewUser input as a parameter and returns a single user. It also contains the uploadProfileImage type, which accepts the ProfileImage and returns a boolean value to indicate the status of the success upload operation.

      Note: Gqlgen automatically defines the Upload scalar type, and it defines the properties of a file. To use it, you only need to declare it at the top of the schema file, as it was done in the code block above.

      At this point, you have defined the structure of the data model for the application. The next step is to generate the schema’s query and the mutation resolver functions using Gqlgen’s code generation feature.

      Step 3 — Generating the Application Resolvers

      In this step, you will use Gqlgen’s code generation feature to automatically generate the GraphQL resolvers based on the schema that you created in the previous step. A resolver is a function that resolves or returns a value for a GraphQL field. This value could be an object or a scalar type such as a string, number, or even a boolean.

      The Gqlgen package is based on a schema-first approach. A time-saving feature of Gqlgen is its ability to generate your application’s resolvers based on your defined schema in the schema.graphqls file. With this feature, you do not need to manually write the resolver boilerplate code, which means you can focus on implementing the defined resolvers.

      To use the code generation feature, execute the command below in the project directory to generate the GraphQL API model files and resolvers:

      A few things will happen after executing the gqlgen command. Two validation errors relating to the schema.resolvers.go file will be printed out, some new files will be generated, and your project will have a new folder structure.

      Execute the tree command to view the new files added to your project.

      tree *
      

      The current directory structure will look similar to this:

      Output

      go.mod go.sum gqlgen.yml graph ├── db.go ├── generated │   └── generated.go ├── model │   └── models_gen.go ├── resolver.go ├── schema.graphqls └── schema.resolvers.go server.go tmp ├── build-errors.log └── main tools.go 2 directories, 8 files

      Among the project files, one important file is schema.resolvers.go. It contains methods that implement the Mutation and Query types previously defined in the schema.graphqls file.

      To fix the validation errors, delete the CreateTodo and Todos methods at the bottom of the schema.resolvers.go file. Gqlgen moved the methods to the bottom of the file because the type definitions were changed in the schema.graphqls file.

      schema.resolvers.go

      
      package graph
      
      // This file will be automatically regenerated based on the schema, any resolver implementations
      // will be copied through when generating and any unknown code will be moved to the end.
      
      import (
          "context"
          "digitalocean/graph/generated"
          "digitalocean/graph/model"
          "fmt"
      )
      
      func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
          panic(fmt.Errorf("not implemented"))
      }
      
      func (r *mutationResolver) UploadProfileImage(ctx context.Context, input model.ProfileImage) (bool, error) {
          panic(fmt.Errorf("not implemented"))
      }
      
      func (r *queryResolver) User(ctx context.Context) (*model.User, error) {
          panic(fmt.Errorf("not implemented"))
      }
      
      // Mutation returns generated.MutationResolver implementation.
      func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
      
      // Query returns generated.QueryResolver implementation.
      func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
      
      type mutationResolver struct{ *Resolver }
      type queryResolver struct{ *Resolver }
      
      // !!! WARNING !!!
      // The code below was going to be deleted when updating resolvers. It has been copied here so you have
      // one last chance to move it out of harms way if you want. There are two reasons this happens:
      //  - When renaming or deleting a resolver the old code will be put in here. You can safely delete
      //    it when you're done.
      //  - You have helper methods in this file. Move them out to keep these resolver files clean.
      
      func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
       panic(fmt.Errorf("not implemented"))
      }
      func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
       panic(fmt.Errorf("not implemented"))
      }
      

      As defined in the schema.graphqls file, Gqlgen’s code generator created two mutations and one query resolver method. These resolvers serve the following purposes:

      • CreateUser: This mutation resolver inserts a new user record into the connected Postgres database.

      • UploadProfileImage: This mutation resolver uploads a media file received from a multipart HTTP request and uploads the file to a bucket within DigitalOcean Spaces. After the file upload, the URL of the uploaded file is inserted into the img_uri field of the previously created user.

      • Users: This query resolver queries the database for all existing users and returns them as the query result.

      Going through the methods generated from the Mutation and Query types, you would observe that they cause a panic with a not implemented error when executed. This indicates that they are still auto-generated boilerplate code. Later in this tutorial, you will return to the schema.resolver.go file to implement these generated methods.

      At this point, you generated the resolvers for this application based on the content of the schema.graphqls file. You will now use the Managed Databases service to create a database that will store the data passed to the mutation resolvers to create a user.

      Step 4 — Provisioning and Using a Managed Database Instance on DigitalOcean

      In this step, you will use the DigitalOcean console to access the Managed Databases service and create a PostgreSQL database to store data from this application. After the database has been created, you will securely store the details in a .env file.

      Although the application will not store images directly in a database, it still needs a database to insert each user‘s record. The stored record will then contain links to the uploaded files.

      A user’s record will consist of a Fullname, email, dateCreated, and an img_uri field of String data type. The img_uri field contains the URL pointing to an image file uploaded by a user through this GraphQL API and stored within a bucket on DigitalOcean Spaces.

      Using your DigitalOcean dashboard, navigate to the Databases section of the console to create a new database cluster, and select PostgreSQL from the list of databases offered. Leave all other settings at their default values and create this cluster using the button at the bottom.

      Digitalocean database cluster

      The database cluster creation process will take a few minutes before it is completed.

      After creating the cluster, follow the Getting Started steps on the database cluster page to set up the cluster for use.

      At the second step of the Getting Started guide, click the Continue, I’ll do this later text to proceed. By default, the database cluster is open to all connections.

      Note: In a production-ready scenario, the Add Trusted Sources input field at the second step should only contain trusted IP addresses, such as the IP Address of the DigitalOcean Droplet running the application. During development, you can alternatively add the IP address of your development machine to the Add Trusted Sources input field.

      Click the Allow these inbound sources button to save and proceed to the next step.

      At the next step, the connection details of the cluster are displayed. You can also find the cluster credentials by clicking the Actions dropdown, then selecting the Connection details option.

      Digitalocean database cluster credentials

      In this screenshot, the gray box at right shows the connection credentials of the created demo cluster.

      You will securely store these cluster credentials as environment variables. In the digitalocean project directory, create a .env file and add your cluster credentials in the following format, making sure to replace the highlighted placeholder content with your own credentials:

      .env

      
       DB_PASSWORD=YOUR_DB_PASSWORD
       DB_PORT=PORT
       DB_NAME=YOUR_DATABASE_NAME
       DB_ADDR=HOST
       DB_USER=USERNAME
      

      With the connection details securely stored in the .env file, the next step will be to retrieve these credentials and connect the database cluster to your project.

      Before proceeding, you will need a database driver to work with Golang’s native SQL package when connecting to the Postgres database. go-pg is a Golang library for translating ORM (object-relational mapping) queries into SQL Queries for a Postgres database. godotenv is a Golang library for loading environment credential from a .env file into your application. Lastly, go.uuid generates a UUID (universally unique identifier) for each user’s record that will be inserted into the database.

      Execute this command to install these:

      • go get github.com/go-pg/pg/v10 github.com/joho/godotenv github.com/satori/go.uuid

      Next, navigate to the graph directory and create a db.go file. You will gradually put together the code within the file to connect with the Postgres database created in the Managed Databases cluster.

      First, add the content of the code block into the db.go file. This function (createSchema) creates a user table in the Postgres database immediately after a connection to the database has been established.

      db.go

      package graph
      
      import (
          "github.com/go-pg/pg/v10"
          "github.com/go-pg/pg/v10/orm"
          "digitalocean/graph/model"
      )
      
      func createSchema(db *pg.DB) error {
          for _, models := range []interface{}{(*model.User)(nil)}{
              if err := db.Model(models).CreateTable(&orm.CreateTableOptions{
                  IfNotExists: true,
              }); err != nil {
                  panic(err)
              }
          }
      
          return nil
      }
      

      Using the IfNotExists option passed to the CreateTable method from go-pg, the createSchema function only inserts a new table into the database if the table does not exist. You can understand this process as a simplified form of seeding a newly created database. Rather than creating the Tables manually through the psql client or GUI, the createSchema function takes care of the table creation.

      Next, add the content of the code block below into the db.go file to establish a connection to the Postgres database and execute the createSchema function above when a connection has been established successfully:

      db.go

      
      import (
            // ...
      
               "fmt" 
               "os" 
          )
      
      func Connect() *pg.DB {
          DB_PASSWORD := os.Getenv("DB_PASSWORD")
          DB_PORT := os.Getenv("DB_PORT")
          DB_NAME := os.Getenv("DB_NAME")
          DB_ADDR := os.Getenv("DB_ADDR")
          DB_USER := os.Getenv("DB_USER")
      
          connStr := fmt.Sprintf(
              "postgresql://%v:%v@%v:%v/%v?sslmode=require",
              DB_USER, DB_PASSWORD, DB_ADDR, DB_PORT, DB_NAME )
      
          opt, err := pg.ParseURL(connStr); if err != nil {
            panic(err)
            }
      
          db := pg.Connect(opt)
      
          if schemaErr := createSchema(db); schemaErr != nil {
              panic(schemaErr)
          }
      
          if _, DBStatus := db.Exec("SELECT 1"); DBStatus != nil {
              panic("PostgreSQL is down")
          }
      
          return db 
      }
      

      When executed, the exported Connect function in the code block above establishes a connection to a Postgres database using go-pg. This is done through the following operations:

      • First, the database credentials you stored in the root .env file are retrieved. Then, a variable is created to store a string formatted with the retrieved credentials. This variable will be used as a connection URI when connecting with the database.

      • Next, the created connection string is parsed to see if the formatted credentials are valid. If valid, the connection string is passed into the connect method as an argument to establish a connection.

      To use the exported Connect function, you will need to add the function to the server.go file, so it will be executed when the application is started. Then the connection can be stored in the DB field within the Resolver struct.

      To use the previously created Connect function from the graph package immediately after the application is started, and to load the credentials from the .env file into the application, open the server.go file in your preferred code editor and add the lines highlighted below:

      Note: Make sure to replace the existing srv variable in the server.go file with the srv variable highlighted below.

      server.go

       package main
      
      import (
        "log"
        "net/http"
        "os"
        "digitalocean/graph"
        "digitalocean/graph/generated"
      
        "github.com/99designs/gqlgen/graphql/handler"
        "github.com/99designs/gqlgen/graphql/playground"
       "github.com/joho/godotenv"
      )
      
      const defaultPort = "8080"
      
      func main() {
           err := godotenv.Load(); if err != nil {
           log.Fatal("Error loading .env file")
          } 
      
        // ...
      
           Database := graph.Connect()
           srv := handler.NewDefaultServer(
                   generated.NewExecutableSchema(
                           generated.Config{
                               Resolvers: &graph.Resolver{
                                   DB: Database,
                               },
                           }),
               )
      
        // ...
      }
      

      In this code snippet, you loaded the credentials stored in the .env through the Load() function. You called the Connect function from the db package and also created the Resolver object with the database connection stored in the DB field. (The stored database connection will be accessed by the resolvers later in this tutorial.)

      Currently, the boilerplate Resolver struct in the resolver.go file does not contain the DB field where you stored the database connection in the code above. You will need to create the DB field.

      In the graph directory, open the resolver.go file and modify the Resolver struct to have a DB field with a go-pg pointer as its type, as shown below:

      resolver.go

      package graph
      
      import "github.com/go-pg/pg/v10"
      
      // This file will not be regenerated automatically.
      //
      // It serves as dependency injection for your app, add any dependencies you require here.
      
      type Resolver struct {
          DB *pg.DB
      }
      

      Now a database connection will be established each time the entry server.go file is run and the go-pg package can be used as an ORM to perform operations on the database from the resolver functions.

      In this step, you created a PostgreSQL database using the Managed Database service on DigitalOcean. You also created a db.go file with a Connect function to establish a connection to the PostgreSQL database when the application is started. Next, you will implement the generated resolvers to store data in the PostgreSQL database.

      Step 5 — Implementing the Generated Resolvers

      In this step, you will implement the methods in the schema.resolvers.go file, which serves as the mutation and query resolvers. The implemented mutation resolvers will create a user and upload the user’s profile image, while the query resolver will retrieve all stored user details.

      Implementing the Mutation Resolver Methods

      In the schema.graphqls file, two mutation resolvers were generated. One with the purpose of inserting the user’s record, while the other handles the profile image uploads. However, these mutations have not yet been implemented as they are boilerplate code.

      Open the schema.resolvers.go file. Modify the imports and the CreateUser mutation with the highlighted lines to insert a new row containing the user details input into the database:

      schema.resolvers.go

      package graph
      
      import (
        "context"
        "fmt"
         "time" 
      
        "digitalocean/graph/generated"
        "digitalocean/graph/model"
        "github.com/satori/go.uuid" 
      )
      
      func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
           user := model.User{ 
               ID:          fmt.Sprintf("%v", uuid.NewV4()), 
               FullName:    input.FullName, 
               Email:       input.Email, 
               ImgURI:      "https://bit.ly/3mCSn2i", 
               DateCreated: time.Now().Format("01-02-2006"), 
           } 
      
           _, err := r.DB.Model(&user).Insert(); if err != nil { 
               return nil, fmt.Errorf("error inserting user: %v", err) 
           } 
      
           return &user, nil 
      }
      
      

      In the CreateUser mutation, there are two things to note about the user rows inserted. First, each row that is inserted is given a UUID. Second, the ImgURI field in each row has a placeholder image URL as the default value. This will be the default value for all records and will be updated when a user uploads a new image.

      Next, you will test the application that has been built at this point. From the project directory, run the server.go file with the following command:

      Now, navigate to http://localhost:8080 through your web browser to access the GraphQL playground built-in to your GraphQL API. Paste the GraphQL Mutation in the code block below into the playground editor to insert a new user record.

      graphql

      
      mutation createUser {
        createUser(
          input: {
            email: "johndoe@gmail.com"
            fullName: "John Doe"
          }
        ) {
          id
        }
      }
      

      The output in the right pane will look similar to this:

      A create user mutation on the GraphQL Playround

      You executed the CreateUser mutation to create a test user with the name of John Doe, and the id of the newly inserted user record was returned as a result of the mutation.

      Note: Copy the id value returned from the executed GraphQL query. You will use the id when uploading a profile image for the test user created above.

      At this point, you have the second UploadProfileImage mutation resolver function left to implement. But before you implement this function, you need to implement the query resolver first. This is because each upload is linked to a specific user, which is why you retrieved the ID of a specific user before uploading an image.

      Implementing the Query Resolver Method

      As defined in the schema.resolvers.graphqls file, one query resolver was generated to retrieve all created users. Similar to the previous mutation resolvers methods, you also need to implement the query resolver method.

      Open scheme.resolvers.go and modify the generated Users query resolver with the highlighted lines. The new code within the Users method below will query the Postgres database for all user rows and return the result.

      schema.resolvers.go

      package graph
      
      func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
        var users []*model.User
      
        err := r.DB.Model(&users).Select()
          if err != nil {
           return nil, err
          } 
      
        return users, nil 
      }
      

      Within the Users resolver function above, fetching all records within the user table is made possible by using go-pg’s select method on the User model without passing the WHERE or LIMIT clause into the query.

      Note: For a bigger application where many records will be returned from the query, it is important to consider paginating the data returned for improved performance.

      To test this query resolver from your browser, navigate to http://localhost:8080 to access the GraphQL playground. Paste the GraphQL Query below into the playground editor to fetch all created user records.

      graphql

      
      query fetchUsers {
        users {
            fullName
            id
            img_uri
        }
      }
      

      The output in the right pane will look similar to this:

      Query result GraphQL playground

      In the returned results, you can see that a users object with an array value was returned. For now, only the previously created user was returned in the users array because that it is the only record in the table. More users will be returned in the users array if you execute the createUser mutation with new details. You can also observe that the img_uri field in the returned data has the hardcoded fallback image URL.

      At this point, you have now implemented both the CreateUser mutation and the User query. Everything is in place for you to receive images from the second UploadProfileImage resolver and upload the received image to a bucket with DigitalOcean Spaces using an S3 compatible AWS-GO SDK.

      Step 6 — Uploading Images to DigitalOcean Spaces

      In this step, you will use the powerful API within the second UploadProfileImage mutation to upload an image to your Space.

      To begin, navigate to the Spaces section of your DigitalOcean console, where you will create a new bucket for storing the uploaded files from your backend application.

      Click the Create New Space button. Leave the settings at their default values and specify a unique name for the new Space:

      Digitalocean spaces

      After a new Space has been created, navigate to the settings tab and copy the Space’s endpoint, name, and region. Add these to the .env file within the GraphQL project in this format:

      .env

      SPACE_ENDPOINT=BUCKET_ENDPOINT
      DO_SPACE_REGION=DO_SPACE_REGION
      DO_SPACE_NAME=DO_SPACE_NAME
      

      As an example, the following screenshot shows the Setting tab, and highlights the name, region, and endpoint details of the demo space (Victory-space):

      Victory-space endpoint, name, and region

      As part of the prerequisites, you created a Space Access key and Secret key for your Space. Paste in your Access and Secret keys into the .env file within the GraphQL application in the following format:

      .env

      ACCESS_KEY=YOUR_SPACE_ACCESS_KEY
      SECRET_KEY=YOUR_SPACE_SECRET_KEY
      

      At this point, you will need to use the CTRL + C key combination to stop the GraphQL server, and execute the command below to restart the GraphQL application with the new credentials loaded into the application.

      Now that your Space credentials are loaded into the application, you will create the upload logic in the UploadProfileImage mutation resolver. The first step will be to add and configure the aws-sdk-go SDK to connect to your DigitalOcean Space.

      One way to programmatically perform operations on your bucket within Spaces is through the use of compatible AWS SDKs. The AWS Go SDK is a development kit that provides a set of libraries to be used by Go developers. The libraries provided by the SDK can be used by a Go written application when performing operations with AWS resources such as file transfers to S3 buckets.

      The DigitalOcean Spaces documentation provides a list of operations you can perform on the Spaces API using an AWS SDK. We will use the aws-sdk-go SDK to connect to the your DigitalOcean Space.

      Execute the go get command to install the aws-sdk-go SDK into the application:

      • go get github.com/aws/aws-sdk-go

      Over the next few code blocks, you will gradually put together the upload logic in the UploadProfileImage mutation resolver.

      First, open the schema.resolvers.go file. Add the highlighted lines to configure the AWS SDK with the stored credentials and establish a connection with your DigitalOcean Space:

      Note: The code within the code block below is incomplete, as you are gradually putting the upload logic together. You will complete the code in the subsequent code blocks.

      schema.resolvers.go

      package graph
      
      import (
         ...
      
         "os"
      
         "github.com/aws/aws-sdk-go/aws"
         "github.com/aws/aws-sdk-go/aws/credentials"
         "github.com/aws/aws-sdk-go/aws/session"
         "github.com/aws/aws-sdk-go/service/s3"
      )
      
      func (r *mutationResolver) UploadProfileImage(ctx context.Context, input model.ProfileImage) (bool, error) {
      
       SpaceRegion := os.Getenv("DO_SPACE_REGION")
       accessKey := os.Getenv("ACCESS_KEY")
       secretKey := os.Getenv("SECRET_KEY")
      
       s3Config := &aws.Config{
           Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
           Endpoint:    aws.String(os.Getenv("SPACE_ENDPOINT")),
           Region:      aws.String(SpaceRegion),
       }
      
       newSession := session.New(s3Config)
       s3Client := s3.New(newSession)
      
      }
      

      Now that the SDK is configured, the next step is to upload the file sent in the multipart HTTP request.

      One way to handle files sent is to read the content from the multipart request, temporarily save the content to a new file in memory, upload the temporary file using the aws-SDK-go library, and then delete it after an upload. Using this approach, a client application such as a web application consuming this GraphQL API still uses the same GraphQL endpoint to perform file uploads, rather than using a third party API to upload files.

      To achieve this, add the highlighted lines to the existing code within the UploadProfileImage mutation resolver in the schema.resolvers.go file:

      schema.resolvers.go

      
      package graph
      
      import (
         ...
      
         "io/ioutil"
         "bytes"
      
      )
      
      func (r *mutationResolver) UploadProfileImage(ctx context.Context, input model.ProfileImage) (bool, error) {
      ...
      
      SpaceName := os.Getenv("DO_SPACE_NAME")
      
      ...
      
      
        userFileName := fmt.Sprintf("%v-%v", input.UserID, input.File.Filename)
        stream, readErr := ioutil.ReadAll(input.File.File)
       if readErr != nil {
           fmt.Printf("error from file %v", readErr)
       }
      
       fileErr := ioutil.WriteFile(userFileName, stream, 0644); if fileErr != nil {
           fmt.Printf("file err %v", fileErr)
       }
      
       file, openErr := os.Open(userFileName); if openErr != nil {
           fmt.Printf("Error opening file: %v", openErr)
       }
      
       defer file.Close()
      
       buffer := make([]byte, input.File.Size)
      
      _, _ = file.Read(buffer)
      
       fileBytes := bytes.NewReader(buffer)
      
       object := s3.PutObjectInput{
           Bucket: aws.String(SpaceName),
           Key:    aws.String(userFileName),
           Body:   fileBytes,
           ACL:    aws.String("public-read"),
       }
      
       if _, uploadErr := s3Client.PutObject(&object); uploadErr != nil {
           return false, fmt.Errorf("error uploading file: %v", uploadErr)
       }
      
       _ = os.Remove(userFileName)
      
      
      return true, nil
      }
      

      Using the ReadAll method from the io package in the code block above, you first read the content of the file added to the multipart request sent to the GraphQL API, and then a temporary file is created to dump this content into.

      Next, using the PutObjectInput struct, you created the structure of the file to be uploaded by specifying the Bucket, Key, ACL, and Body field to be the content of the temporarily stored file.

      Note: The Access Control List (ACL) field in the PutObjectInput struct has a public-read value to make all uploaded files available for viewing over the internet. You can remove this field if your application requires that uploaded data be kept private.

      After creating the PutObjectInput struct, the PutObject method is used to make a PUT operation, sending the values of the PutObjectInput struct to the bucket. If there is an error, a false boolean value and an error message are returned, ending the execution of the resolver function and the mutation in general.

      To test the upload mutation resolver, you can use an image of Sammy the Shark, DigitalOcean’s mascot. Use the wget command to download an image of Sammy:

      • wget https://html.sammy-codes.com/images/small-profile.jpeg

      Next, execute the cURL command below to make an HTTP request to the GraphQL API to upload Sammy’s image, which has been added to the request form body.

      Note: If you are on a Windows Operating System, it is recommended that you execute the cURL commands using the Git Bash shell due to the backslash escapes.

      • curl localhost:8080/query -F operations="{ "query": "mutation uploadProfileImage($image: Upload! $userId : String!) { uploadProfileImage(input: { file: $image userId : $userId}) }", "variables": { "image": null, "userId" : "12345" } }" -F map='{ "0": ["variables.image"] }' -F 0=@small-profile.jpeg

      Note: We are using a random userId value in the request above because the process of updating a user’s record has not yet been implemented.

      The output will look similar to this, indicating that the file upload was successful:

      Output

      {"data": { "uploadProfileImage": true }}

      In the Spaces section of the DigitalOcean console, you will find the image uploaded from your terminal:

      A bucket within Digitalocean showing a list of uploaded files

      At this point, file uploads within the application are working; however, the files are linked to the user who performed the upload. The goal of each file upload is to have the file uploaded into a storage bucket and then linked back to a user by updating the img_uri field of the user.

      Open the resolver.go file in the graph directory and add the code block below. It contains two methods: one to retrieve a user from the database by a specified field, and the other function to update the record of a user.

      resolver.go

      
      import (
      ...
      
        "digitalocean/graph/model"
        "fmt"
      )
      
      ...
      
      func (r *mutationResolver) GetUserByField(field, value string) (*model.User, error) {
          user := model.User{}
      
          err := r.DB.Model(&user).Where(fmt.Sprintf("%v = ?", field), value).First()
      
          return &user, err
      }
      
      
      func (r *mutationResolver) UpdateUser(user *model.User) (*model.User, error) {
          _, err := r.DB.Model(user).Where("id = ?", user.ID).Update()
          return user, err
      }
      
      

      The first GetUserByField function above accepts a field and value argument, both of a string type. Using go-pg’s ORM, it executes a query on the database, fetching data from the user table with a WHERE clause.

      The second UpdateUser function in the code block uses go-pg to execute an UPDATE statement to update a record in the user table. Using the where method, a WHERE clause with a condition is added to the UPDATE statement to update only the row having the same ID passed into the function.

      Now you can use the two methods in the UploadProfileImage mutation. Add the content of the highlighted code block below to the UploadProfileImage mutation within the schema.resolvers.go file. This will retrieve a specific row from the user table and update the img_uri field in the user’s record after the file has been uploaded.

      Note: Place the highlighted code at the line above the existing return statement within the UploadProfileImage mutation.

      schema.resolvers.go

      
      package graph
      
      
      func (r *mutationResolver) UploadProfileImage(ctx context.Context, input model.ProfileImage) (bool, error) {
        _ = os.Remove(userFileName)
      
       
          user, userErr := r.GetUserByField("ID", *input.UserID)
        
           if userErr != nil {
               return false, fmt.Errorf("error getting user: %v", userErr)
           }
        
         fileUrl := fmt.Sprintf("https://%v.%v.digitaloceanspaces.com/%v", SpaceName, SpaceRegion, userFileName)
        
           user.ImgURI = fileUrl
        
           if _, err := r.UpdateUser(user); err != nil {
               return false, fmt.Errorf("err updating user: %v", err)
           }
        
      
        return true, nil
      }
      

      From the new code added to the schema.resolvers.go file, an ID string and the user’s ID are passed to the GetUserByField helper function to retrieve the record of the user executing the mutation.

      A new variable is then created and given the value of a string formatted to have the link of the recently uploaded file in the format of https://BUCKET_NAME.SPACE_REGION.digitaloceanspaces.com/USER_ID-FILE_NAME. The ImgURI field in the retrieved user model was reassigned the value of the formatted string as a link to the uploaded file.

      Paste the curl command below into your terminal, and replace the highlighted USER_ID placeholder in the command with the userId of the user created through the GraphQL playground in a previous step. Make sure the userId is wrapped in quotation marks so that the terminal can encode the value properly.

      • curl localhost:8080/query -F operations="{ "query": "mutation uploadProfileImage($image: Upload! $userId : String!) { uploadProfileImage(input: { file: $image userId : $userId}) }", "variables": { "image": null, "userId" : "USER_ID" } }" -F map='{ "0": ["variables.image"] }' -F 0=@small-profile.jpeg

      The output will look similar to this:

      Output

      {"data": { "uploadProfileImage": true }}

      To further confirm that the user’s img_uri was updated, you can use the fetchUsers query from the GraphQL playground in the browser to retrieve the user’s details. If the update was successful, you will see that the default placeholder URL of https://bit.ly/3mCSn2i in the img_uri field has been updated to the value of the uploaded image.

      The output in the right pane will look similar to this:

      A query mutation to retrieve an updated user record using the GraphQL Playground

      In the returned results, the img_uri in the first user object returned from the query has a value that corresponds to a file upload to a bucket within DigitalOcean Spaces. The link in the img_uri field is made up of the bucket endpoint, the user’s ID, and lastly, the filename.

      To test the permission of the uploaded file set through the ACL option, you can open the img_uri link in your browser. Due to the default Metadata on the uploaded image, it will automatically download to your computer as an image file. You can open the file to view the image.

      Downloaded view of the uploaded file

      The image at the img_uri link will be the same image that was uploaded from the command line, indicating that the methods in the resolver.go file were executed correctly, and the entire file upload logic in the UploadProfileImage mutation works as expected.

      In this step, you uploaded an image into a DigitalOcean Space by using the AWS SDK for Go from the UploadProfileImage mutation resolver.

      Conclusion

      In this tutorial, you performed a file upload to a created bucket on a DigitalOcean Space using the AWS SDK for Golang from a mutation resolver in a GraphQL application.

      As a next step, you could deploy the application built within this tutorial. The Go Dev Guide provides a beginner-friendly guide on how to deploy a Golang application to DigitalOcean’s App Platform, which is a fully managed solution for building, deploying, and managing your applications from various programming languages.



      Source link

      How To Make Your Vue.js Application DRY with Slots, Mixins, and Composition API


      The author selected Open Sourcing Mental Illness to receive a donation as part of the Write for DOnations program.

      Introduction

      DRY is a programming strategy that stands for “Don’t Repeat Yourself”. It encourages code re-usability through modular architecture, where code is re-used rather than repeated. This often results in code that is dynamic and scalable. In short, this principle guides programmers to avoid repeating code and hardcoding any values in your application.

      Vue.js includes several strategies to modularize and re-use repeated snippets of code. In this tutorial, you will try out these strategies by making a sample Vue.js application DRY. The tutorial will introduce ways to keep your template and script DRY within your component. You will use layout components that establish an HTML structure that you can place content into via slots. Then, you will use mixins, which are JavaScript files that contain data, methods, and computed properties that mix together with existing component options. Finally, you are going to use the new Composition API that was introduced in Vue 3. The Composition API is a different way to structure your components, and promotes the unification of component properties.

      Prerequisites

      Step 1 — Setting Up the Example Application

      To illustrate how you can create scalable and DRY Vue.js code, you’ll first set up an example application. This example application will be a main/detail application that shows a list of airport cards. When clicked, these cards will navigate you to another view with additional details on that airport.

      First, you’ll need to create a new Vue.js application. Do this by running the following command in your terminal:

      • vue create favorite-airports

      When prompted, select Manually select features. The following options to select are: Choose Vue version, Babel, and Router. Once selected, hit the RETURN key and continue filling out the prompts as follows:

      Output

      Vue CLI v4.5.15 ? Please pick a preset: Manually select features ? Check the features needed for your project: Choose Vue version, Babel, Router ? Choose a version of Vue.js that you want to start the project with 3.x ? Use history mode for router? (Requires proper server setup for index fallback in production) Yes ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files

      Once the favorite-airports project is created, open your terminal window and cd (change directory) into the favroite-airports root folder. Once you’ve changed the directory, create a new directory to hold all of your local data for this project:

      Inside this folder, create a new Javascript file named src/data/airports.js and open it in your text editor of choice. To provide your app with sample data, add the following contents to the file:

      favorite-airports/src/data/airports.js

      export default [
        {
          name: 'Cincinnati/Northern Kentucky International Airport',
          abbreviation: 'CVG',
          city: 'Hebron',
          state: 'KY',
          destinations: {
            passenger: [ 'Toronto', 'Seattle/Tacoma', 'Austin', 'Charleston', 'Denver', 'Fort Lauderdale', 'Jacksonville', 'Las Vegas', 'Los Angeles', 'Baltimore', 'Chicago', 'Detroit', 'Dallas', 'Tampa' ],
            cargo: [ 'Anchorage', 'Baltimore', ' Chicago' , 'Indianapolis', 'Phoenix', 'San Francisco', 'Seattle', 'Louisville', 'Memphis' ]
          }
        },
        {
          name: 'Seattle-Tacoma International Airport',
          abbreviation: 'SEA',
          city: 'Seattle',
          state: 'WA',
          destinations: {
            passenger: [ 'Dublin', 'Mexico City', 'Vancouver', 'Albuquerque', 'Atlanta', 'Frankfurt', 'Amsterdam', 'Salt Lake City', 'Tokyo', 'Honolulu' ],
            cargo: [ 'Spokane', 'Chicago', 'Dallas', ' Shanghai', 'Cincinnati', 'Luxenbourg', 'Anchorage', 'Juneau', 'Calgary', 'Ontario' ]
          }
        },
        {
          name: 'Minneapolis-Saint Paul International Airport',
          abbreviation: 'MSP',
          city: 'Bloomington',
          state: 'MN',
          destinations: {
            passenger: [ 'Dublin', 'Paris', 'Punta Cana', 'Winnipeg', 'Tokyo', 'Denver', 'Tulsa', 'Washington DC', 'Orlando', 'Mexico City' ],
            cargo: [ 'Cincinnati', 'Omaha', 'Winnipeg', 'Chicago', 'St. Louis', 'Portland', 'Philadelphia', 'Milwaukee', 'Ontario' ]
          }
        }
      ]
      

      This is an array of objects consisting of a few airports in the United States. In the main view of this application, you are going to iterate through this data to generate cards consisting of the name, abbreviation, city, and state properties.

      Save data/airports.js and return to the terminal.

      When you’ve completed that step, create a single-file component (SFC) with the name AirportCard.vue. This file will live in the components directory of your project and will contain all the styles and logic for the airport cards. Open AirportCard.vue in your text editor and add the following:

      favorite-airports/src/components/AirportCard.vue

      <template>
        <div class="airport">
          <p>{{ airport.abbreviation }}</p>
          <p>{{ airport.name }}</p>
          <p>{{ airport.city }}, {{ airport.state }}</p>
        </div>
      </template>
      
      <script>
      export default {
        props: {
          airport: {
            type: Object,
            required: true
          }
        }
      }
      </script>
      
      <style scoped>
      .airport {
        border: 3px solid;
        border-radius: .5rem;
        padding: 1rem;
        margin-bottom: 1rem;
      }
      
      .airport p:first-child {
        font-weight: bold;
        font-size: 2.5rem;
        margin: 1rem 0;
      }
      
      .airport p:last-child {
        font-style: italic;
        font-size: .8rem;
      }
      </style>
      

      You may notice that there is some CSS included in this code snippet. In the AirportCard.vue component, the wrapper <div> contains the class of airport. This CSS adds some styling to the generated HTML by adding borders to give each airport the appearance of a “card”. The :first-child and :last-child are pseudo selectors that apply different styling to the first and last <p> tags in the HTML inside the <div> element with the class of airport. In addition to that, you may also notice that this component contains a prop, which in Vue.js is a way to pass data down from a parent component to a child component.

      Save and exit from the file.

      Before wrapping up this setup, replace the existing views/Home.vue component code with the following:

      favorite-airports/src/views/Home.vue

      <template>
        <div class="wrapper">
          <div v-for="airport in airports" :key="airport.abbreviation">
            <airport-card :airport="airport" />
          </div>
        </div>
      </template>
      
      <script>
      import allAirports from '@/data/airports.js'
      import AirportCard from '@/components/AirportCard.vue'
      
      export default {
        components: {
          AirportCard
        },
        data() {
          return {
            airports: allAirports
          }
        }
      }
      </script>
      
      <style>
      #app {
        font-family: Avenir, Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
        margin-top: 60px;
      }
      
      .wrapper {
        display: grid;
        grid-template-columns: 1fr 1fr 1fr;
        grid-column-gap: 1rem;
        max-width: 960px;
        margin: 0 auto;
      }
      
      p,
      h3 {
        grid-column: span 3;
      }
      </style>
      

      This code contains a v-for loop that iterates through the airports.js data and renders a series of AirportCards.vue components with airport data passed in via the prop :airport. Save this code and return to the command line.

      With the project set up, run the local development server using the npm run serve command in your terminal:

      This will start a server on your localhost, usually at port 8080. Open your web browser of choice and visit localhost:8080 to see the following:

      A list of cards rendered in Vue using the v-for directive.

      Now that your example application is set up, in the next step you are going to create two different Vue.js components that you can later use as page layouts.

      Step 2 — Using slot to Create Layout Components

      Layout components are components that use slot elements to compose HTML templates that can be re-used with different content. These are great for when you have multiple templates that you want to re-use, such as a two-column or three-column layout.

      To create a layout component, you’ll first create a directory for them to live in. You could put them in the components folder, but since these components have a very specific job, your project will be more legible to other programmers if you differentiate them. Create a directory called layouts in the src directory:

      Next, create a file in your layouts directory named DefaultLayout.vue. The DefaultLayout.vue component will be a wrapper that contains the view’s content and centers it in the browser window. Open DefaultLayout.vue in your text editor and add the following:

      favorite-airports/src/layouts/DefaultLayout.vue

      <template>
        <div class="default-layout">
          <slot />
        </div>
      </template>
      
      <style scoped>
        .default-layout {
          max-width: 960px;
          margin: 0 auto;
        }
      </style>
      

      This component is a div with a class of default-layout. You can leverage this class to add some styles. The CSS styles you see in the component above restrict its width to a max of 960px, with side margins being automatic. This will center the div horizontally in the browser window. The slot element is a default slot. Anything that is placed between two <layout-default> tags will be injected to where this <slot /> is. You can try this out by refactoring the Home.vue that you modified in the previous step.

      Save your DefaultLayout.vue file. In your text editor, open src/views/Home.vue and import the DefaultLayout.vue component.

      favorite-airports/src/views/Home.vue

      ...
      <script>
      import allAirports from '@/data/airports.js'
      import AirportCard from '@/components/AirportCard.vue'
      import DefaultLayout from '@/layouts/DefaultLayout.vue'
      
      export default {
        components: {
          AirportCard,
          DefaultLayout
        },
        data() {
          return {
            airports: allAirports
          }
        }
      }
      </script>
      ...
      

      With the DefaultLayout.vue component imported, you can now replace the containing <div /> with <default-layout />.

      favorite-airports/src/views/Home.vue

      <template>
        <default-layout class="wrapper">
          <div v-for="airport in airports" :key="airport.abbreviation">
            <airport-card :airport="airport" />
          </div>
        </default-layout>
      </template>
      ...
      

      You can now remove the max-width and margin properties in the .wrapper class that contain and center the div. Save this file, and open localhost:8080 in your browser window. Visually, nothing will change, but you now have a new layout component to contain any content in a centered div.

      Before you move on to the next step, you will create one more layout component. This is going to be a two-column layout. One column will be for supplemental information, and the other will be for the main content of the view. Create a new file at src/layouts/TwoColumnLayout.vue. Once created, open TwoColumnLayout.vue component in your text editor and add the following:

      favorite-airports/src/layouts/TwoColumnLayout.vue

      <template>
        <div class="two-column-layout">
          <aside>
            <slot name="sideBar" />
          </aside>
          <main>
            <slot name="content" />
          </main>
        </div>
      </template>
      
      <style>
        .two-column-layout {
          display: grid;
          grid-template-columns: 1fr 1fr 1fr;
          grid-column-gap: 1rem;
        }
      
        .two-column-layout aside,
        .two-column-layout main {
          border: 1px solid;
          border-radius: 5px;
        }
      
        .two-column-layout aside {
          grid-column: span 1;
        }
      
        .two-column-layout main {
          grid-column: span 2;
        }
      </style>
      

      In this component, you have two named slots, one for the sidebar and the other for the main content. On the containing <div>, you are using CSS to create a grid of three columns, with one spanning one column and the other spanning two.

      To use this layout, create a new view for the airport detail view at src/views/AirportDetail.vue, then add the following code to the new file:

      favorite-airports/src/views/AirportDetail.vue

      <template>
        <two-column-layout>
          <template v-slot:sideBar>
            <p>Sidebar</p>
          </template>
          <template v-slot:content>
            <p>Main Content</p>
          </template>
        </two-column-layout>
      </template>
      
      <script>
      import TwoColumnLayout from '@/layouts/TwoColumnLayout.vue'
      
      export default {
        components: {
          TwoColumnLayout
        },
      }
      </script>
      

      This new view imports TwoColumnLayout.vue then uses v-slot to fill the named slots with the right content.

      Save this file. To make this view viewable, add a route in the Vue router file:

      favorite-airports/src/router/index.js

      import { createRouter, createWebHistory } from 'vue-router'
      import Home from '../views/Home.vue'
      import AirportDetail from '../views/AirportDetail'
      
      const routes = [
        {
          path: '/',
          name: 'Home',
          component: Home
        },
        {
          path: '/airport/:code',
          name: 'AirportDetail',
          component: AirportDetail
        },
        {
          path: '/about',
          name: 'About',
          // route level code-splitting
          // this generates a separate chunk (about.[hash].js) for this route
          // which is lazy-loaded when the route is visited.
          component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
        }
      ]
      ...
      

      Here you are registering a route that, when visited, will load the AirportDetail.vue component. The :code in the path is an argument that you can leverage later to grab a specific airport’s data.

      Save your file, then open your browser to localhost:8080/airport/cvg. You will find the following:

      The airport detail view, with a sidebar box and a main content box.

      In this step, you created layout components by leveraging slots. These layout components can help keep your app DRY by eliminating duplicate code when creating a structure of the webpage. In the next step, you are going try out mixins to share methods and properties between components.

      Step 3 — Using Mixins to Share Methods and Properties

      Mixins are a way to redistribute reusable component options to any number of components. When a mixin is imported, the mixin’s component options are “mixed in” to the current component. To illustrate this, this step will first run through an example of mixin syntax, and will then add mixins into your example app.

      Say you have two files with unique properties. The first has a data method and a computed property, like the following:

      sample-component

      <script>
        export default {
          data() {
            return {
              firstName: 'Dave',
              lastName: 'Berning'
            }
          },
          computed: {
            fullName() {
              return `${this.firstName} ${this.lastName}`
            }
          }
        }
      </script>
      

      The second is a file containing some component options that you want to re-use:

      someMixin

      export default {
        data() {
          return {
            counter: 0
          }
        },
        methods: {
          increment() {
            this.counter++
          }
        }
      }
      

      You can mix these two files together by importing the mixin (someMixin) into the component (sample-component). In this hypothetical component, you import it with the import keyword and assign it using the mixin property:

      sample-component

      <script>
      import someMixin from '@/mixins/someMixin'
      
      export default {
        data() {
          return {
            firstName: 'Dave',
            lastName: 'Berning'
          }
        },
        mixins: [ 'someMixin' ],
        computed: {
          fullName() {
            return `${this.firstName} ${this.lastName}`
          }
        }
      }
      </script>
      

      When imported, the hypothetical component has access to all methods, data, computed properties, and any other component options that it might contain.

      Next, you will create a mixin that contains a method and a data property. This function will combine the airport name and abbreviation and store it into a data property.

      In your terminal, create a new directory using the mkdir command:

      Make a file named src/mixins/airport.js and export an object containing the follow properties:

      src/mixins/airport.js

      export default {
        data() {
          return {
            airportWithCode: ''
          }
        },
        methods: {
          getAirportWithCode(airport) {
            this.airportWithCode = `${airport.name} - ${airport.abbreviation}`
          }
        }
      }
      

      This object will now have a data method and a method sets the data to an airport’s name and abbreviation. Save this file.

      With this created, import it into the Home.vue view. You are going to leverage this method and data property to display the string that is returned when the user clicks a card:

      src/views/Home.vue

      <template>
        <default-layout class="wrapper">
          <div v-for="airport in airports" :key="airport.abbreviation" @click="getAirportWithCode(airport)">
            <airport-card :airport="airport" />
          </div>
          <p>test: {{ airportWithCode }}</p>
        </default-layout>
      </template>
      
      <script>
      import allAirports from '@/data/airports.js'
      import AirportCard from '@/components/AirportCard.vue'
      import DefaultLayout from '@/layouts/DefaultLayout.vue'
      import airportMixin from '@/mixins/airport.js'
      
        export default {
          components: {
            AirportCard,
            DefaultLayout
          },
          mixins: [ airportMixin ],
          data() {
            return {
              airports: allAirports
            }
          }
        }
      </script>
      ...
      

      Since you have access to the methods and data within that mixin, you can reference them like any other component option. You’ve done this in this code snippet to store an airportWithCode value when the user clicks a card, then render the string value in a paragraph element. Save the file.

      Next, re-use this same mixin in AirportDetail.vue. Open AirportDetail.vue in your text editor, then write a JavaScript filter to return the airport object if the abbreviation matches the :code argument that was defined in the router earlier:

      src/views/AirportDetail.vue

      ...
      <script>
      import TwoColumnLayout from '@/layouts/TwoColumnLayout.vue'
      import allAirports from '@/data/airports.js'
      
      export default {
        components: {
          TwoColumnLayout
        },
        data() {
          return {
            airport: ''
          }
        },
        methods: {
        getAirportByCode() {
          return allAirports.filter(airport => airport.abbreviation === this.$route.params.code.toUpperCase())[0]
          }
        },
        mounted() {
          this.airport = this.getAirportByCode()
        }
      }
      </script>
      

      In this snippet, you are creating a new method named getAirportByCode that filters through the airport data and returns the airport object whose abbreviation matches the abbreviation in the route URL. On mounting, you are assigning the airport data property to that object that is returned.

      Save the file. Next, import the mixin you used earlier. You’ll leverage the same data and method properties you did before:

      src/views/AirportDetail.vue

      <template>
        <two-column-layout>
          <template v-slot:sideBar>
            <p>Sidebar</p>
          </template>
          <template v-slot:content>
            <p>Main Content</p>
            <p>{{ airportWithCode }}</p>
          </template>
        </two-column-layout>
      </template>
      
      <script>
      import TwoColumnLayout from '@/layouts/TwoColumnLayout.vue'
      import allAirports from '@/data/airports.js'
      import airportMixin from '@/mixins/airport.js'
      
      export default {
        components: {
          TwoColumnLayout
        },
        mixins: [ airportMixin ],
        data() { ... },
        methods: { ... },
        mounted() {
          this.airport = this.getAirportByCode()
          this.getAirportWithCode(this.airport)
        }
      }
      </script>
      

      Since you have determined the specific airport with getAirportByCode() and set it to this.airport, you can now use the getAirportWithCode method to set the value of airportWithCode from the mixin. You can then display the value by adding that variable to the template.

      Save this file, and open localhost:8080/airport/cvg in the browser window. You will find the string value of airportWithCode rendered in the main content section, as shown in the following image:

      Detail view of the CVG airport, with the string

      In this step, you used mixins to share methods and computed properties between components. Mixins can be a great way to organize components and leverage reusable code throughout your application. Next, you are going to learn about the new Composition API, why it was created, and how it can improve the composition of your next Vue application.

      Step 4 — Using the Composition API

      The mixins and layout components that you have tried out so far in this tutorial are usable in all major versions of Vue, including Vue 2 and earlier. These constitute the Options API. But in Vue 3 there is another API you can use to make your application DRY: the Composition API.

      The Composition API is a new way to set up components. Instead of having separate sections for data, computed, methods, and props, you have a single setup hook that everything lives in. Within this setup property, everything the component needs to operate before it is created will go in here. Additionally, everything needed to define options in the Options API needs to be imported. But this is not so for the Composition API.

      In this step, you are going to refactor one of your components from using the Options API to the new Composition API.

      In your text editor, open the AirportDetail.vue component. Right now, you have a mixin imported into this component. That mixin provides some functions and data properties. But in the Composition API, everything that your component needs to render will live inside the setup method; there’s no need to import this method.

      Remove your mixin import, and add the setup method under mounted:

      favorite-airports/src/views/AirportDetail.vue

      ...
      <script>
      import TwoColumnLayout from '@/layouts/TwoColumnLayout.vue'
      import allAirports from '@/data/airports.js'
      
      export default {
        components: { ... },
        methods: {
          getAirportByCode() {
            return allAirports.filter(airport => airport.abbreviation === this.$route.params.code.toUpperCase())[0]
          }
        },
        mounted() { ... },
        setup() {
      
        }
      }
      </script>
      

      With your setup method created, start refactoring this by adding the new onMounted lifecycle hook. The onMounted function accepts one argument, typically an anonymous function:

      favorite-airports/src/views/AirportDetail.vue

      ...
      <script>
      import TwoColumnLayout from '@/layouts/TwoColumnLayout.vue'
      import allAirports from '@/data/airports.js'
      import { onMounted } from 'vue'
      
      export default {
        components: { ... },
        methods: {
          getAirportByCode() {
            return allAirports.filter(airport => airport.abbreviation === this.$route.params.code.toUpperCase())[0]
          }
        },
        mounted() { ... },
        setup() {
          onMounted(() => {
      
          })
        }
      }
      </script>
      

      When this component is mounted, you’ll store the current airport object into a reactive data property. To make a variable or constant reactive, you need to wrap the value in a ref function, which you will import from vue. The name of the constant will be the name of your reference in your setup and template sections of the component. Also, remove the getAirportByCode from methods and define it like a regular JavaScript function inside of setup:

      favorite-airports/src/views/AirportDetail.vue

      ...
      <script>
      import TwoColumnLayout from '@/layouts/TwoColumnLayout.vue'
      import allAirports from '@/data/airports.js'
      import { onMounted, ref } from 'vue'
      
      export default {
        components: { ... },
        mounted() { ... },
        setup() {
          function getAirportByCode() {
            return allAirports.filter(airport => airport.abbreviation === this.$route.params.code.toUpperCase())[0]
          }
      
          onMounted(() => {
            const airport = ref(getAirportByCode())
          })
        }
      }
      </script>
      

      After you do that, you can go ahead and delete the old mounted and methods properties from the component file.

      Since you aren’t using a mixin now, you will define getAirportWithCode inside of your setup function and assign it to a variable airportWithCode so you can use it in your view:

      favorite-airports/src/views/AirportDetail.vue

      ...
      <script>
      import TwoColumnLayout from '@/layouts/TwoColumnLayout.vue'
      import allAirports from '@/data/airports.js'
      import { onMounted, ref } from 'vue'
      
      export default {
        components: {
          TwoColumnLayout
        },
        setup() {
          const airportWithCode = ref('')
      
          function getAirportByCode() {
            return allAirports.filter(airport => airport.abbreviation === this.$route.params.code.toUpperCase())[0]
          }
      
          function getAirportWithCode(airport) {
            return `${airport.name} - ${airport.abbreviation}`
          }
      
            onMounted(() => {
              const airport = ref(getAirportByCode())
            })
          }
        }
      </script>
      

      One very important thing about reactive data properties with the Composition API is that ref returns an object. To access the values, you need to access its .value property:

      favorite-airports/src/views/AirportDetail.vue

      ...
      <script>
      import TwoColumnLayout from '@/layouts/TwoColumnLayout.vue'
      import allAirports from '@/data/airports.js'
      import { onMounted, ref } from 'vue'
      
      export default {
        components: {
          TwoColumnLayout
        },
        setup() {
          const airportWithCode = ref('')
      
          function getAirportByCode() {
            return allAirports.filter(airport => airport.abbreviation === this.$route.params.code.toUpperCase())[0]
          }
      
          function getAirportWithCode(airport) {
            return `${airport.name} - ${airport.abbreviation}`
          }
      
          onMounted(() => {
            const airport = ref(getAirportByCode())
            airportWithCode.value = getAirportWithCode(airport.value)
          })
        }
      }
      </script>
      

      There are now two things that you need to do before this can be completely converted to using the Composition API. The first thing you need to change is this.$route in the getAirportByCode function. In the Composition API, you cannot access the route or router with this.$route and this.$router, respectively.

      To access the route, import the useRoute from the vue-router package. It’s better to save this into a const that you can reference throughout the application:

      favorite-airports/src/views/AirportDetail.vue

      ...
      <script>
      import TwoColumnLayout from '@/layouts/TwoColumnLayout.vue'
      import allAirports from '@/data/airports.js'
      import { onMounted, ref } from 'vue'
      import { useRoute } from 'vue-router'
      
      export default {
        components: {
          TwoColumnLayout
        },
        setup() {
          const route = useRoute()
          const airportWithCode = ref('')
      
          function getAirportByCode() {
            return allAirports.filter(airport => airport.abbreviation === route.params.code.toUpperCase())[0]
          }
      
          function getAirportWithCode(airport) {
            return `${airport.name} - ${airport.abbreviation}`
          }
      
          onMounted(() => {
            const airport = ref(getAirportByCode())
            airportWithCode.value = getAirportWithCode(airport.value)
          })
        }
      }
      </script>
      

      When that is done, return the object in your setup function. The properties returned in this object can be used in the template:

      favorite-airports/src/views/AirportDetail.vue

      ...
        setup() {
          const route = useRoute()
          const airportWithCode = ref('')
      
          function getAirportByCode() {
            return allAirports.filter(airport => airport.abbreviation === route.params.code.toUpperCase())[0]
          }
      
          function getAirportWithCode(airport) {
            return `${airport.name} - ${airport.abbreviation}`
          }
      
          onMounted(() => {
            const airport = ref(getAirportByCode())
            airportWithCode.value = getAirportWithCode(airport.value)
          })
      
          return { airportWithCode }
        }
      }
      </script>
      

      Save your code and reload localhost:8080/airport/cvg in your browser. After refactoring the code, there will be no change in what is rendered. However, you are now taking advantage of the Composition API.

      Conclusion

      In this tutorial, you tried out a few strategies to make your application DRY. Specifically, you re-used layout components in multiple views, then modularized properties and methods with mixins. Finally, you re-factored your app to use the new Composition API that was introduced in Vue 3. This API is a new way to set up components before they get created, making functional in more situations.

      If you’d like to learn more about Mixins or the Composition API, it’s highly encouraged to review the official Vue documentation. For more tutorials on Vue, check out the How To Develop Websites with Vue.js series page.



      Source link