DEV Community

Ridhwana Khan for The DEV Team

Posted on • Edited on

Documenting Forem's v1 API

Forem has set a milestone to update our (v1) API documentation and we need YOUR help!

There are several endpoints that we would like to document in order to complete our v0 -> v1 upgrade. v0 will eventually be deprecated and removed (there aren't any breaking changes so existing endpoints will continue to work the same as before). If you’re looking to contribute to open source, these are awesome first issues to work on and this post will help guide you through them.

In this post, I’ll outline the details about our v1 API, we’ll discuss its documentation and then I’ll walk you through an example of an API endpoint that I had recently documented.

About the v1 API

The API is a REST (REpresentational State Transfer) API, which means that it follows a set of guiding principles that you can read more about here.

There are currently many resources that can be accessed via the API, however, it does not contain all of the resources that we have available on DEV. We are continuously adding more endpoints to the API.

It is important to note that all operations are scoped to a user at the moment, and one cannot interact with the API as an organization.

Headers

The API consists of both authenticated and non-authenticated endpoints.

Most read endpoints do not require authentication and can be accessed without API keys. However, we require authentication for most endpoints that can create, update or delete resources, and those that contain more private information.

CORS (Cross Origin Resource Sharing) is disabled on authenticated endpoints, however endpoints that do not require authentication have an open CORS Policy. With an open CORS policy you are able to access some endpoints from within a browser script without needing to connect via a server or backend.

Authentication is granted via a valid API Key. The API key can be generated by logging into your account at dev.to and clicking on Generate API Key on the Settings (Extensions) page.

Image description

Once you’ve generated your API key, you are ready to start interacting with the API. The API Key will be set as a header, namely api-key, on a request.

We require another header for accessing the v1 API - the Accept header. The Accept header needs to be set to application/vnd.forem.api-v1+json, where v1 is the version of the API. If you do not pass along an Accept header you will automatically be routed to v0 of the API. API (v0) will be deprecated soon and we encourage you to rather use v1.

Accessing the API

As mentioned above, there are some API endpoints that are authenticated and others that do not require authentication.

When interacting with an endpoint that does not require authentication, you can pass through a single header (the Accept header) that will set the version of the API that you are interacting with.

curl -X GET https://dev.to/api/articles\?page\=1 --header "accept: application/vnd.forem.api-v1+json"
Enter fullscreen mode Exit fullscreen mode

Image description

An endpoint that requires authentication would need both the Accept and the api-key header to be set when making the request:

curl -X PUT http: //localhost :3000/api/users/1/unpublish
--header "api-key: <your-api-key>"
--header "accept: application/vnd.forem.api-v1+json" -V
Enter fullscreen mode Exit fullscreen mode

You need to have the correct roles and permissions set on your user to be able to query certain data from the API. For example, only admins can read, create, update and delete Display Ad resources.

About our documentation

Our v1 API endpoints are documented according to the OpenAPI Specification.

The OpenAPI Specification

The OpenAPI Specification (previously known as a Swagger Specification) is a standard for defining RESTful interfaces. As per the definition on their website, it is a document (or set of documents) that defines or describes an API.

An OpenAPI definition uses and conforms to the OpenAPI Specification. The OpenAPI definition can be created within your codebase.

You can view the Open API Specification here. It describes API Versions, Formats, Document Structure, Data Types, Schemas and much more.

When an API adheres to the Open API specification, it allows opportunities to use document generation tools to display the API, code generation tools to generate servers and clients in various programming languages, access to testing tools etc. Some of these tools include Swagger UI, Redoc, DapperDox and RapidDoc.

Forem, which is a Ruby on Rails app, integrates the Open API Specification via a gem - the rswag gem. The rswag Ruby gem allows us to create a DSL for describing and testing our API operations. It also extends rspec-rails "request specs", hence, allowing our documentation to be a part of our test suite which allows us to make requests with test parameters and seed data that invoke different response codes. As a result, we are able to test what the requests and responses look like, however we do not test the business logic that drives the endpoint - that is tested elsewhere in the code.

Once we write the test, we are able to generate a JSON file that conforms to the Open API Specification thus allowing us to eventually use the document generation tools to format and beautify our documentation.

The OpenAPI definition in the form of api_v1.json is generated and used as input to Docusaurus to create our documentation. You can view our v1 documentation here.

Now that we’ve discussed how Open API and rswag fit together to create the API documentation let’s work through an example of adding documentation to an endpoint together.

Documenting a v1 endpoint

We’ll be working on this github issue together. The issue outlines a task to use rswag to document the /api/followers/users endpoint in the v1 API.

For reference, our v1 documentation lives here.

There is also a pull request for the code written in the example below which you can reference.

Skeleton

Let’s start by creating a file for the endpoint that we want to test and document.We can go ahead and create spec/requests/api/v1/docs/followers_spec.rb

There are some building blocks for each test - a skeleton that defines some standards, implementation details like generating the header values, seed data etc.

Below is a code snippet of the skeleton for this test:

require "rails_helper"
require "swagger_helper"

# rubocop:disable RSpec/EmptyExampleGroup
# rubocop:disable RSpec/VariableName

RSpec.describe "Api::V1::Docs::Followers" do
 let(:Accept) { "application/vnd.forem.api-v1+json" }
 let(:api_secret) { create(:api_secret) }
 let(:user) { api_secret.user }
 let(:follower1) { create(:user) }
 let(:follower2) { create(:user) }

 before do
   follower1.follow(user)
   follower2.follow(user)
   user.reload
 end

 describe "GET /followers/users" do
   path "/api/followers/users" do
     get "Followers" do
     end
   end
 end
end

# rubocop:enable RSpec/EmptyExampleGroup
# rubocop:enable RSpec/VariableName
Enter fullscreen mode Exit fullscreen mode

We start off by importing the necessary libraries - in this case the rails_helper and the swagger_helper that will allow us to use the DSL to build out our definitions.

If you’re use RSpec before then the describe block will be familiar to you, it will create an example group.

let(:Accept) { "application/vnd.forem.api-v1+json" }
let(:api_secret) { create(:api_secret) }
Enter fullscreen mode Exit fullscreen mode

Above, we define (but not yet set) the header values. The accept header will allow us to access the v1 API and since the /api/followers/users endpoint requires authentication we generate an API secret that we will use later on.

let(:user) { api_secret.user }
let(:follower1) { create(:user) }
let(:follower2) { create(:user) }

 before do
   follower1.follow(user)
   follower2.follow(user)
   user.reload
 end
Enter fullscreen mode Exit fullscreen mode

Above, we use RSpec to setup our data so that we can have example responses in our API documentation. We create a user and two follower users. With these models in the DB rswag will run the tests and display the example tags created by FactoryBot in the json file. In the before block, we then setup the two follows to follow the user.

describe "GET /followers/users" do
   path "/api/followers/users" do
     get "Followers" do
     end
   end
end
Enter fullscreen mode Exit fullscreen mode

In a nested describe block we start by specifying the path for the endpoint that we’re testing which is /api/followers/users. You can read more about path operations in the rswag documentation.

In some circumstances, you may have an identifier or parameter in the path. These are surrounded by curly braces. For example: /api/articles/{id}.

Operation Ids

get "Followers" do
end
Enter fullscreen mode Exit fullscreen mode

The above is considered our operation block. In this instance we define that we are implementing a GET.

The Operation Object has several fields that can be set to help define this endpoint. You can read more about operation objects here.

These are some of the fields that we want to define for api/followers/users:

get "Followers" do
  tags "followers"
  description(<<-DESCRIBE.strip)
  This endpoint allows the client to retrieve a list of the followers they have.
  "Followers" are users that are following other users on the website.
  It supports pagination, each page will contain 80 followers by default.
  DESCRIBE
  operationId "getFollowers"
  produces "application/json"
end
Enter fullscreen mode Exit fullscreen mode

Below, I’ve taken some of the definitions from the specification and applied it to the code sample to help explain what each field does.

tags: A list of tags for API documentation control. They are used for logical grouping of operations by resources or any other qualifier. In this case, we want this endpoint to be grouped on its own and so we provide it with a new tag. In other circumstances, you may want to tag your crud operations for a single resource all with the same tag so that they can logically group together.

description: Provides a verbose explanation of the operation behavior. CommonMark syntax may be used for rich text representation. We try to describe what the endpoint does in a single sentence. Thereafter, we can provide additional context that we think will be useful to the user of the API at a glance.

operationId: This is a unique string used to identify the operation. The id must be unique among all operations described in the API. The operationId value is case-sensitive. Tools and libraries may use the operationId to uniquely identify an operation, therefore, it is recommended to follow common programming naming conventions. We try to structure the work with the CRUD operation as the prefix and the resource as the suffix.

produces: This field populates the response content type with the produces property of the OpenAPI definition. Our response type is usually JSON.

Another noteworthy field that we add to other endpoints but is not relevant to this particular endpoint is:

security: This is a declaration of which security mechanisms can be used across the API. Individual operations can override this definition.

We define a security scheme globally in swagger_helper.rb levelsecurity: [{ "api-key": [] }],. The security scheme that we utilize applies authentication via an api-key header.

However, remember earlier I mentioned that not all endpoints need authentication. Hence, for those that do not need authentication, we can override the "security" attribute at the operation level. To do this, we provide an empty security requirement ({}) to the array.

If you look at /api/articles in the article_spec you will see security [] which indicates that this endpoint does not need authentication.

However, in the case of the endpoint that we’re documenting /api/followers/users we do not need to provide a security field as we’ll use the one that is defined globally with the API key for authentication via an api key.

Now that we’ve set these operationIds, let’s have a look at how they would get generated in the JSON file.

Image description

The operationIds set the scene for how the API operates. Next, we’ll want to define the parameters that the API endpoint allows.

The following two sections, Parameters and Response Blocks, rely on schemas, so before we get into the details of these sections let’s first discuss what a schema is.

The OpenAPI Specification allows you to describe something called a "schema" which in its simplest form refers to some JSON structure. These can be defined either inline with your operation descriptions OR as referenced globals.

You can use a referenced global when you have a repeating schema in multiple operation specs. For example, an article resource may be returned in a create, read or update, hence instead of repeating this JSON across all these operations you could add it as a referenced global in the swagger_heper.rb and then use that $ref.

The global definitions section lets you define common data structures used in your API. They can be referenced via $ref: "a schema object" – both for request body and response body.

Another instance where you may want to use a referenced global is when multiple endpoints accept the same parameter schema and you do not want to repeat this JSON for multiple endpoints.

Parameters

If you look at the code for the api/followers/users endpoint, you’ll notice that it takes three optional parameters; page, per_page and sort.

You’ll notice that we use page and per_page across multiple endpoints for our pagination strategy hence that reusability makes it the ideal candidate for a referenced global.

Since it’s been used before, you’ll find it defined globally in the swagger_helper.rb. Hence, all we need to do is reference it in our spec.

parameter "$ref": "#/components/parameters/pageParam"
parameter "$ref": "#/components/parameters/perPageParam30to1000"
Enter fullscreen mode Exit fullscreen mode

However, our next parameter - sort, has not been defined before and does not seem to be re-used in the same manner across any existing endpoints. Thus we can define it inline.

    parameter name: :sort, in: :query, required: false,
              description: "Default is 'created_at'. Specifies the sort order for the created_at param of the follow
                               relationship. To sort by newest followers first (descending order) specify
                               ?sort=-created_at.",
              schema: { type: :string },
              example: "created_at"
Enter fullscreen mode Exit fullscreen mode

You can read more about describing query parameters in the OpenAPI Guide here

This is what the final set of query parameters look like:

get "Followers" do
  ......
  parameter "$ref": "#/components/parameters/pageParam"
  parameter "$ref": "#/components/parameters/perPageParam30to1000"
  parameter name: :sort, in: :query, required: false,
            description: "Default is 'created_at'. Specifies the sort order for the created_at param of the follow
                               relationship. To sort by newest followers first (descending order) specify
                               ?sort=-created_at.",
            schema: { type: :string },
            example: "created_at"
end
Enter fullscreen mode Exit fullscreen mode

And this is what the corresponding generated JSON would look like:

Image description

Response Blocks

Once we’ve defined the operation Ids and Parameters, we can define what the response query looks like. We can create multiple response blocks in order to test the various responses a user of the API may receive. This includes testing when the API endpoint provides a response, when there is no content, when the user is not authorized etc.

In this case, we’ll test what a successful response looks like and what an unauthorized response looks like when we do not provide the correct api-key.

A successful response with status code 200

response "200", "A List of followers" do
  let(:"api-key") { api_secret.secret }
  schema type: :array,
         items: {
           description: "A user (follower)",
           type: "object",
           properties: {
             type_of: { description: "user_follower by default", type: :string },
             id: { type: :integer, format: :int32 },
             user_id: { description: "The follower's user id", type: :integer, format: :int32 },
             name: { description: "The follower's name", type: :string },
             path: { description: "A path to the follower's profile", type: :string },
             profile_image: { description: "Profile image (640x640)", type: :string }
             }
   add_examples


   run_test!
end
Enter fullscreen mode Exit fullscreen mode

The HTTP 200 OK success status response code indicates that the request has succeeded.

In order for the request to succeed, we first need to provide the necessary authentication. When this example runs, it will need the api-key for authentication, hence we set it in our RSpec test.

In this case, I’ve decided to define the schema object inline because it is a uniquely structured response that is not being shared with other endpoints. However, if more than one endpoint had the same schema it would have been beneficial to define it globally in the swagger_helper and then provide a reference to it in the various spec files.

  schema type: :array,
         items: {
           description: "A user (follower)",
           type: "object",
           properties: {
             type_of: { description: "user_follower by default", type: :string },
             id: { type: :integer, format: :int32 },
             user_id: { description: "The follower's user id", type: :integer, format: :int32 },
             name: { description: "The follower's name", type: :string },
             path: { description: "A path to the follower's profile", type: :string },
             profile_image: { description: "Profile image (640x640)", type: :string }
          }
        }
Enter fullscreen mode Exit fullscreen mode

The schema that we have defined above is an array of objects. The top level type is defined by an array type and each item is an object. Thereafter, we further define the properties that can be expected in each object. We advise that a description for a property is added where necessary.

If the need to re-use the schema object for multiple endpoints arose, we could have defined it as a Follower in the swagger_helper.spec and then referenced it in our spec like below:

schema type: :array,
        items: { "$ref": "#/components/schemas/Follower" }
Enter fullscreen mode Exit fullscreen mode

The add_examples method can be found in the swagger_helper and it is responsible for creating the Response Object.

It creates a map containing descriptions of potential response payloads.
The key is a media type or media type range like application/json and the value describes it.

Finally, the run_test! method is called within each response block. This tells rswag to create and execute a corresponding example. It builds and submits a request based on parameter descriptions and corresponding values that have been provided using the rspec "let" syntax. In order for our examples to add value, we want to give it a good set of seed data.

If you want to do additional validation on the response, you can pass a block to the run_test! method.

You can read more about how to use run_test! from the rswag documentation.

An unauthorized response with status code 401

response "401", "unauthorized" do
  let(:"api-key") { nil }
  add_examples

  run_test!
end
Enter fullscreen mode Exit fullscreen mode

The HyperText Transfer Protocol (HTTP) 401 Unauthorized response status code indicates that the client request has not been completed because it lacks valid authentication credentials for the requested resource. Hence, in this case if we provide an invalid api key, we should expect a 401.

To test this case, we simply provide an invalid API key which will not allow us to authenticate to the API.

The generated JSON for these two responses look as follows:

Image description

Generating the JSON

I’ve been referencing the JSON files above, but you must be wondering how do you access that JSON. Once you have written your spec, you can generate the JSON file for the API by running

SWAGGER_DRY_RUN=0 RAILS_ENV=test rails rswag PATTERN="spec/requests/api/v1/**/*_spec.rb"
Enter fullscreen mode Exit fullscreen mode

Once you do this, you will see a newly generate file at https://github.com/forem/forem/blob/main/swagger/v1/api_v1.json.

Take the time to evaluate the generated content in this file, especially for the new spec. In order to view it you may paste the JSON into https://editor.swagger.io/. When you do this, it will display the data as documentation and also let you know if there are any errors.

If you have Visual Studio Code, we suggest you install the following extensions that enable validation and navigation within the spec file:

And that, my friends, is how we document API v1 endpoints at Forem.

That's all folks

You can find the code for this example here.

If you have any questions or feedback, please drop them in the comments below. If you’d like to contribute to our documentation please have a look through the ones that aren’t assigned in this milestone and raise your hand on the issue. We look forward to your contributions.

Top comments (11)

Collapse
 
kgilpin profile image
Kevin Gilpin • Edited

Are you interested in generating OpenAPI directly from your test cases? Here’s a post about doing exactly this for Mastodon:

dev.to/appmap/automatically-genera...

It uses your regular test suite - you don’t have to rewrite or rearrange tests like you do with RSwag.

Collapse
 
ridhwana profile image
Ridhwana Khan

Ooooh this looks interesting - thanks for dropping it here @kgilpin, I'll give it a read 👀.

Collapse
 
kgilpin profile image
Kevin Gilpin

Hey, I just wanted to let you know that we've prepared a PR showing how to generate OpenAPI for Forem with AppMap - github.com/forem/forem/pull/19041

In the PR description you'll find information about how it works, and some instances of how the generated OpenAPI can enhance the definitions that are already in the Forem repo.

Collapse
 
thomasbnt profile image
Thomas Bnt

Using OpenAI is a great tool to see how we can play with datas. Such a good idea! Can't wait when the version is released to updating some projects like

GitHub logo thomasbnt / devtoprofile

An example of getting data from the dev.to API to display its own articles.

Made with VueJS GitHub last commit Depfu Discord GitHub Sponsors Twitter Follow

Getting data from the API of DEV.TO

An example of getting data from the dev.to API to display its own articles. Work with VueJS, fetch and the API v0 of dev.to (this version will be DEPRECIATED. See the post on DEV).

Note

See the preview of this project here →

Preview of this project

Note

More projects like that ? Check this list.

See the list of awesome projects with an API

How to get my data ?

Change this lines in the file src/components/devto.vue :

const USERID_DEVTO = '18254'
const USERNAME_DEVTO = 'thomasbnt'
Enter fullscreen mode Exit fullscreen mode

How to get my ID ?

Get your ID by using the website. Press F12, and on the body element, you have a data-user attribute. This is your ID.

How to get my dev.to ID


How to develop this project

Project setup

yarn install
Enter fullscreen mode Exit fullscreen mode

Compiles and hot-reloads for development

yarn serve
Enter fullscreen mode Exit fullscreen mode

Compiles and minifies for production

yarn build
Enter fullscreen mode Exit fullscreen mode

Lints and fixes files

yarn lint
Enter fullscreen mode Exit fullscreen mode

Customize configuration

See Configuration Reference.




Collapse
 
ridhwana profile image
Ridhwana Khan

Us too! 🔥

Collapse
 
jeremyfiel profile image
Jeremy Fiel

This is a great and very thorough article. I'm def interested in contributing. Thanks for sharing.

A couple things to clarify: Swagger has been renamed as OpenAPI since 2015. Any reference to the specification should use OpenAPI. Swagger remains many products offered by SmartBear. Unfortunately, many tools in the ecosystem used the swagger moniker in their package names and haven't been updated. I see you tried to explain it but the terms should not be used interchangeably.

smartbear.com/blog/what-is-the-dif...

Secondly, now would be a great time to move to OAS3.1.x to take advantage of the full JSON Schema support. This allows usage of many of JSON Schema's features from draft 2020-12 and beyond, without limitation. Whereas OAS 3.0.x has quite a few limitations to design proper JSON Schemas. Not to mention it's based on a modified draft-04 version which is quite old.

Collapse
 
andypiper profile image
Andy Piper

Yup, I came to comment the exact same thing - should be starting from OAS rather than Swagger / aka “OAS pre-OAS” 😌 happy to help with some of this work, so I’ll go ahead and talk a look at the related GitHub issue!

Collapse
 
ridhwana profile image
Ridhwana Khan

Thanks @andypiper. I went ahead an updated the article to make it a bit more clear and removed the confusing references where appropriate to the previous name (Swagger).

We're looking forward to your contribution ✨

Collapse
 
ridhwana profile image
Ridhwana Khan

Hi @jeremyfiel 👋🏽 Thanks so much for bringing that to my attention, I went ahead an updated the article to make it a bit more clear and removed the confusing references where appropriate to the previous name (Swagger).

Secondly, now would be a great time to move to OAS3.1.x to take advantage of the full JSON Schema support.

That's a really good point, I'll bring it up to the team, we'll chat more about it and what it entails. Hopefully, we can upgrade soon!

Thanks again, we're looking forward to your contribution ✨

Collapse
 
fyodorio profile image
Fyodor

Interesting read and interesting approach to API documenting Ridhwana, thanks 👍

Talking about the OAS file you mentioned in the post, for me it was really interesting to audit it with 42Crunch audit tool (I work there but I'm a UI developer and not selling it just in case, full disclosure 😄 and it's free anyway). If you check out the API docs with it (either through UI or using the VS Code plugin), you may find that first, the audit is blocked by a structural JSON issue (schema error):

Image description

and second, if you fix that quickly, there are some issues of different criticality which would be not very hard to fix, provided with the decent remediation instructions:

Image description

Again, I'm not a dev advocate of any kind, I just personally use the tool for my pet project and it helps me a lot, as I'm not a BE dev. And I truly wish Forem to become better and better, being a contributer myself for a couple of times 😅

Collapse
 
ndimares profile image
Nolan Di Mare Sullivan

@ridhwana have you thought about adding SDKs for the Forem API? It could help let with usability and integration speed. For some ideas, check otu the Forem SDKs I created in Go, Python, TS, and Typescript from your OpenAPI spec.

If you're interested, I could help you set up automation so that every time a new version of your OpenAPI spec is published, a new version of the SDKs are published. Let me know! Really admire the work you and the forem team do.