mirror of https://github.com/zulip/zulip.git
338 lines
14 KiB
Markdown
338 lines
14 KiB
Markdown
# Documenting REST API endpoints
|
|
|
|
This document explains the system for documenting [Zulip's REST
|
|
API](https://zulip.com/api/rest).
|
|
|
|
Zulip's API documentation is an essential resource both for users and
|
|
for the developers of Zulip's mobile and terminal apps. Our vision is
|
|
for the documentation to be sufficiently good that developers of
|
|
Zulip's apps should never need to look at the server's implementation
|
|
to answer questions about the API's semantics.
|
|
|
|
To achieve these goals, Zulip leverages the popular OpenAPI format as
|
|
the data source to ensure that Zulip's API documentation is correct
|
|
and remains so as Zulip's API evolves.
|
|
|
|
In particular, the top goal for this system is that all mistakes in
|
|
verifiable content (i.e. not the English explanations) should cause
|
|
the Zulip test suite to fail. This is incredibly important, because
|
|
once you notice one error in API documentation, you no longer trust it
|
|
to be correct, which ends up wasting the time of its users.
|
|
|
|
Since it's very difficult to not make little mistakes when writing any
|
|
untested code, the only good solution to this is a way to test
|
|
the documentation. We found dozens of errors in the process of adding
|
|
the validation Zulip has today.
|
|
|
|
Our API documentation is defined by a few sets of files:
|
|
|
|
- The primary source of our API documentation is the Zulip server's
|
|
[OpenAPI description](openapi.md) at
|
|
`zerver/openapi/zulip.yaml`.
|
|
- The documentation is written the same Markdown framework that powers
|
|
our [help center docs](helpcenter.md), with some special
|
|
extensions for rendering nice code blocks and example
|
|
responses. Most API endpoints share a common template,
|
|
`templates/zerver/api/api-doc-template.md`, which renders the
|
|
OpenAPI description of the API endpoint. A handful of endpoints that
|
|
require special content, as well as pages that document general API
|
|
details rather than specific endpoints, live at
|
|
`templates/zerver/api/*.md`.
|
|
- We have an extensive set of tests designed to validate that the data
|
|
in the OpenAPI file matching the implementation. Specifically,
|
|
`zerver/tests/test_openapi.py` compares every endpoint's accepted
|
|
parameters in `views` code with those declared in `zulip.yaml`. And
|
|
the [backend test suite](../testing/testing-with-django.md) checks
|
|
that every API response served during our extensive backend test
|
|
suite matches one the declared OpenAPI schema for that endpoint.
|
|
- The text for the Python examples comes from a test suite for the
|
|
Python API documentation (`zerver/openapi/python_examples.py`; run via
|
|
`tools/test-api`). The `generate_code_example` macro will magically
|
|
read content from that test suite and render it as the code example.
|
|
This structure ensures that Zulip's API documentation is robust to a
|
|
wide range of possible typos and other bugs in the API
|
|
documentation.
|
|
- The JavaScript examples are similarly generated and tested using
|
|
`zerver/openapi/javascript_examples.js`.
|
|
- The cURL examples are generated and tested using
|
|
`zerver/openapi/curl_param_value_generators.py`.
|
|
- The REST API index
|
|
(`templates/zerver/help/include/rest-endpoints.md`) in the broader
|
|
/api left sidebar (`templates/zerver/api/sidebar_index.md`).
|
|
|
|
This first section is focused on explaining how the API documentation
|
|
system is put together; when actually documenting an endpoint, you'll
|
|
want to also read the [Step by step guide](#step-by-step-guide).
|
|
|
|
## How it works
|
|
|
|
To understand how this documentation system works, start by reading an
|
|
existing doc file (`templates/zerver/api/render-message.md` is a good
|
|
example; accessible live
|
|
[here](https://zulip.com/api/render-message) or in the development
|
|
environment at `http://localhost:9991/api/render-message`).
|
|
|
|
We highly recommend looking at those resources while reading this page.
|
|
|
|
If you look at the documentation for existing endpoints, you'll notice
|
|
that a typical endpoint's documentation is divided into four sections:
|
|
|
|
- The top-level **Title and description**
|
|
- **Usage examples**
|
|
- **Arguments**
|
|
- **Responses**
|
|
|
|
The rest of this guide describes how each of these sections works.
|
|
|
|
### Title and description
|
|
|
|
Displayed at the top of any REST endpoint documentation page, the
|
|
title comes from the `summary` parameter in OpenAPI data. The
|
|
description should explain what the endpoint does in clear
|
|
English. Include details on how to use it correctly or what it's good
|
|
or bad for, with links to any alternative endpoints the user might
|
|
want to consider.
|
|
|
|
These sections should often contain a link to the documentation of the
|
|
relevant feature in `/help/`.
|
|
|
|
### Usage examples
|
|
|
|
We display usage examples in three languages: Python, JavaScript and
|
|
`curl`; we may add more in the future. Every endpoint should have
|
|
Python and `curl` documentation; `JavaScript` is optional as we don't
|
|
consider that API library to be fully supported. The examples are
|
|
defined using a special Markdown extension
|
|
(`zerver/openapi/markdown_extension.py`). To use this extension, one
|
|
writes a Markdown file block that looks something like this:
|
|
|
|
```md
|
|
{start_tabs}
|
|
{tab|python}
|
|
|
|
{generate_code_example(python)|/messages/render:post|example}
|
|
|
|
{tab|js}
|
|
...
|
|
|
|
{tab|curl}
|
|
|
|
{generate_code_example(curl)|/messages/render:post|example}
|
|
|
|
{end_tabs}
|
|
```
|
|
|
|
In some cases, one wants to configure specific parameters to be
|
|
included or excluded from the example `curl` requests for readability
|
|
reasons. One can do that using the `x-curl-examples-parameters`
|
|
parameter.
|
|
|
|
#### Writing Python examples
|
|
|
|
For the Python examples, you'll write the example in
|
|
`zerver/openapi/python_examples.py`, and it'll be run and verified
|
|
automatically in Zulip's automated test suite. The code there will
|
|
look something like this:
|
|
|
|
```python
|
|
@openapi_test_function('/messages/render:post')
|
|
def render_message(client: Client) -> None:
|
|
# {code_example|start}
|
|
# Render a message
|
|
request = {
|
|
'content': '**foo**'
|
|
}
|
|
result = client.render_message(request)
|
|
# {code_example|end}
|
|
|
|
validate_against_openapi_schema(result, '/messages/render', 'post', '200')
|
|
```
|
|
|
|
This is an actual Python function which will be run as part of the
|
|
`tools/test-api` test suite. The `validate_against_opanapi_schema`
|
|
function will verify that the result of that request is as defined in
|
|
the examples in `zerver/openapi/zulip.yaml`.
|
|
|
|
To run as part of the test suite, the `render_message` function needs
|
|
to be called from `test_messages` (or one of the other functions at
|
|
the bottom of the file). The final function, `test_the_api`, is what
|
|
actually runs the tests. Tests with the `openapi_test_function`
|
|
decorator that are not called will fail tests, as will new endpoints
|
|
that are not covered by an `openapi_test_function`-decorated test.
|
|
|
|
You will still want to manually test the example using Zulip's Python
|
|
API client by copy-pasting from the website; it's easy to make typos
|
|
and other mistakes where variables are defined outside the tested
|
|
block, and the tests are not foolproof.
|
|
|
|
The code that renders `/api` pages will extract the block between the
|
|
`# {code_example|start}` and `# {code_example|end}` comments, and
|
|
substitute it in place of
|
|
`{generate_code_example(python)|/messages/render:post|example}`
|
|
wherever that string appears in the API documentation.
|
|
|
|
- Additional Python imports can be added using the custom
|
|
`x-python-examples-extra-imports` field in the OpenAPI definition.
|
|
- Endpoints that only administrators can use should be tagged with the
|
|
custom `x-requires-administrator` field in the OpenAPI definition.
|
|
|
|
### Parameters
|
|
|
|
We have a separate Markdown extension to document the parameters that
|
|
an API endpoint supports. You'll see this in files like
|
|
`templates/zerver/api/render-message.md` via the following Markdown
|
|
directive (implemented in
|
|
`zerver/lib/markdown/api_arguments_table_generator.py`):
|
|
|
|
```md
|
|
{generate_api_arguments_table|zulip.yaml|/messages/render:post}
|
|
```
|
|
|
|
Just as in the usage examples, the `/messages/render` key must match a
|
|
URL definition in `zerver/openapi/zulip.yaml`, and that URL definition
|
|
must have a `post` HTTP method defined.
|
|
|
|
Additional content that you'd like to appear in the parameter
|
|
description area can be declared using the custom
|
|
`x-parameter-description` field in the OpenAPI definition.
|
|
|
|
### Displaying example payloads/responses
|
|
|
|
If you've already followed the steps in the [Usage examples](#usage-examples)
|
|
section, this part should be fairly trivial.
|
|
|
|
You can use the following Markdown directive to render all the fixtures
|
|
defined in the OpenAPI `zulip.yaml` for a given endpoint
|
|
|
|
```md
|
|
{generate_code_example|/messages/render:post|fixture}
|
|
```
|
|
|
|
Additional content that you'd like to appear in the responses part of
|
|
the page can be added using the custom `x-response-description` field
|
|
in the OpenAPI definition.
|
|
|
|
## Step by step guide
|
|
|
|
This section offers a step-by-step process for adding documentation
|
|
for a new API endpoint. It assumes you've read and understood the
|
|
above.
|
|
|
|
1. Start by adding [OpenAPI format](openapi.md)
|
|
data to `zerver/openapi/zulip.yaml` for the endpoint. If you
|
|
copy-paste (which is helpful to get the indentation structure
|
|
right), be sure to update all the content that you copied to
|
|
correctly describe your endpoint!
|
|
|
|
In order to do this, you need to figure out how the endpoint in
|
|
question works by reading the code! To understand how arguments
|
|
are specified in Zulip backend endpoints, read our [REST API
|
|
tutorial][rest-api-tutorial], paying special attention to the
|
|
details of `REQ` and `has_request_variables`.
|
|
|
|
Once you understand that, the best way to determine the supported
|
|
arguments for an API endpoint is to find the corresponding URL
|
|
pattern in `zprojects/urls.py`, look up the backend function for
|
|
that endpoint in `zerver/views/`, and inspect its arguments
|
|
declared using `REQ`.
|
|
|
|
You can check your formatting using these helpful tools.
|
|
|
|
- `tools/check-openapi` will verify the syntax of `zerver/openapi/zulip.yaml`.
|
|
- `tools/test-backend zerver/tests/test_openapi.py`; this test compares
|
|
your documentation against the code and can find many common
|
|
mistakes in how arguments are declared.
|
|
- `test-backend`: The full Zulip backend test suite will fail if
|
|
any actual API responses generated by the tests don't match your
|
|
defined OpenAPI schema. Use `test-backend --rerun` for a fast
|
|
edit/refresh cycle when debugging.
|
|
|
|
[rest-api-tutorial]: ../tutorials/writing-views.md#writing-api-rest-endpoints
|
|
|
|
1. Add a function for the endpoint you'd like to document to
|
|
`zerver/openapi/python_examples.py`, decorated with
|
|
`@openapi_test_function`. `render_message` is a good example to
|
|
follow. There are generally two key pieces to your test: (1) doing
|
|
an API query and (2) verifying its result has the expected format
|
|
using `validate_against_openapi_schema`.
|
|
|
|
1. Make the desired API call inside the function. If our Python
|
|
bindings don't have a dedicated method for a specific API call,
|
|
you may either use `client.call_endpoint` or add a dedicated
|
|
function to the [zulip PyPI
|
|
package](https://github.com/zulip/python-zulip-api/tree/main/zulip).
|
|
Ultimately, the goal is for every endpoint to be documented the
|
|
latter way, but it's useful to be able to write working
|
|
documentation for an endpoint that isn't supported by
|
|
`python-zulip-api` yet.
|
|
|
|
1. Add the function to one of the `test_*` functions at the end of
|
|
`zerver/openapi/python_examples.py`; this will ensure your
|
|
function will be called when running `test-api`.
|
|
|
|
1. Capture the JSON response returned by the API call (the test
|
|
"fixture"). The easiest way to do this is add an appropriate print
|
|
statement (usually `json.dumps(result, indent=4, sort_keys=True)`),
|
|
and then run `tools/test-api`. You can also use
|
|
<https://jsonformatter.curiousconcept.com/> to format the JSON
|
|
fixtures. Add the fixture to the `example` subsection of the
|
|
`responses` section for the endpoint in
|
|
`zerver/openapi/zulip.yaml`.
|
|
|
|
1. Run `./tools/test-api` to make sure your new test function is being
|
|
run and the tests pass.
|
|
|
|
1. Now, inside the function, isolate the lines of code that call the API and could
|
|
be displayed as a code example. Wrap the relevant lines in
|
|
`# {code_example|start} ... relevant lines go here ... # {code_example|end}`
|
|
comments. The lines inside these comments are what will be displayed as the
|
|
code example on our `/api` page.
|
|
|
|
1. Finally, if the API docs page of the endpoint doesn't follow the
|
|
common API docs template in
|
|
`templates/zerver/api/api-docs-template.md`, then add its custom
|
|
Markdown file under `templates/zerver/api/`. However, it is a goal
|
|
to minimize the number of files that diverse from the common
|
|
template, so only do this if there's a good reason.
|
|
|
|
1. Add the endpoint to the index in
|
|
`templates/zerver/help/include/rest-endpoints.md`. The URL should
|
|
match the `operationId` for the endpoint, and the link text should
|
|
match the title of the endpoint from the OpenAPI `summary` field.
|
|
|
|
1. Test your endpoint, pretending to be a new user in a hurry, by
|
|
visiting it via the links on `http://localhost:9991/api` (the API
|
|
docs are rendered from the Markdown source files on page load, so
|
|
just reload to see an updated version as you edit). You should
|
|
make sure that copy-pasting the code in your examples works, and
|
|
post an example of the output in the pull request.
|
|
|
|
1. Document the new API in `templates/zerver/api/changelog.md` and
|
|
bump the `API_FEATURE_LEVEL` in `version.py`. Also, make sure to
|
|
add a `**Changes**` entry in the description of the new API/event
|
|
in `zerver/openapi/zulip.yaml`, which mentions the API feature level
|
|
at which they were added.
|
|
|
|
[javascript-examples]: https://github.com/zulip/zulip-js/tree/main/examples
|
|
|
|
## Why a custom system?
|
|
|
|
Given that our documentation is written in large part using the
|
|
OpenAPI format, why maintain a custom Markdown system for displaying
|
|
it? There's several major benefits to this system:
|
|
|
|
- It is extremely common for API documentation to become out of date
|
|
as an API evolves; this automated testing system helps make it
|
|
possible for Zulip to maintain accurate documentation without a lot
|
|
of manual management.
|
|
- Every Zulip server can host correct API documentation for its
|
|
version, with the key variables (like the Zulip server URL) already
|
|
pre-substituted for the user.
|
|
- We're able to share implementation language and visual styling with
|
|
our Help Center, which is especially useful for the extensive
|
|
non-REST API documentation pages (e.g. our bot framework).
|
|
|
|
Using the standard OpenAPI format gives us flexibility, though; if we
|
|
later choose to migrate to third-party tools, we don't need to redo
|
|
the actual documentation work in order to migrate tools.
|