В последних версиях Postman появилась возможность добавлять пользовательское визуальное представление для ответа на запрос к API. Например, отобразить список объектов не как JSON, а в виде таблицы или карточек:

json-view

table-view

cards-view

Эта статья даст краткую вводую информацию о том, как этим пользоваться.

Для начала

Для того, чтобы сделать минимальный пример потребуется два действия:

  1. Создать новый запрос в Postman,
  2. Создать шаблон, который будет применяться к ответу.

Как создавать запросы я рассказывал в первой статье о Postman.

Для примера к этой статье буду обращаться к API themoviedb.com — сервиса с рекомендациями фильмов и сериалов. Конкретно к методу получения списка популярных на текущий момент фильмов:

GET https://api.themoviedb.org/3/movie/popular?language=en-US&page=1&api_key={{api_key}}

Переменная api_key хранит значение ключа для авторизации в API. Получить ключ можно в настройках профиля после регистрации на сайте https://www.themoviedb.org/.

На случай если ответ этого метода изменится или вы не захотите регистрироваться и получать токен для авторизации, то ниже представлен полный ответ этого API-метода.

{
    "page": 1,
    "results": [
        {
            "adult": false,
            "backdrop_path": "/srYya1ZlI97Au4jUYAktDe3avyA.jpg",
            "genre_ids": [
                14,
                28,
                12
            ],
            "id": 464052,
            "original_language": "en",
            "original_title": "Wonder Woman 1984",
            "overview": "Wonder Woman comes into conflict with the Soviet Union during the Cold War in the 1980s and finds a formidable foe by the name of the Cheetah.",
            "popularity": 7833.944,
            "poster_path": "/8UlWHLMpgZm9bx6QYh0NFoq67TZ.jpg",
            "release_date": "2020-12-16",
            "title": "Wonder Woman 1984",
            "video": false,
            "vote_average": 7.3,
            "vote_count": 1766
        },
        {
            "adult": false,
            "backdrop_path": "/kf456ZqeC45XTvo6W9pW5clYKfQ.jpg",
            "genre_ids": [
                16,
                35,
                18,
                10402,
                14
            ],
            "id": 508442,
            "original_language": "en",
            "original_title": "Soul",
            "overview": "Joe Gardner is a middle school teacher with a love for jazz music. After a successful gig at the Half Note Club, he suddenly gets into an accident that separates his soul from his body and is transported to the You Seminar, a center in which souls develop and gain passions before being transported to a newborn child. Joe must enlist help from the other souls-in-training, like 22, a soul who has spent eons in the You Seminar, in order to get back to Earth.",
            "popularity": 4698.966,
            "poster_path": "/hm58Jw4Lw8OIeECIq5qyPYhAeRJ.jpg",
            "release_date": "2020-12-25",
            "title": "Soul",
            "video": false,
            "vote_average": 8.5,
            "vote_count": 2100
        },
        {
            "adult": false,
            "backdrop_path": "/cjaOSjsjV6cl3uXdJqimktT880L.jpg",
            "genre_ids": [
                12,
                14,
                10751,
                16
            ],
            "id": 529203,
            "original_language": "en",
            "original_title": "The Croods: A New Age",
            "overview": "Searching for a safer habitat, the prehistoric Crood family discovers an idyllic, walled-in paradise that meets all of its needs. Unfortunately, they must also learn to live with the Bettermans -- a family that's a couple of steps above the Croods on the evolutionary ladder. As tensions between the new neighbors start to rise, a new threat soon propels both clans on an epic adventure that forces them to embrace their differences, draw strength from one another, and survive together.",
            "popularity": 1982.471,
            "poster_path": "/tK1zy5BsCt1J4OzoDicXmr0UTFH.jpg",
            "release_date": "2020-11-25",
            "title": "The Croods: A New Age",
            "video": false,
            "vote_average": 7.8,
            "vote_count": 848
        },
        {
            "adult": false,
            "backdrop_path": "/riy51I77gAw3KyPfceihkSHxaHY.jpg",
            "genre_ids": [
                878,
                12
            ],
            "id": 517096,
            "original_language": "ru",
            "original_title": "Вратарь Галактики",
            "overview": "Cosmoball is a mesmerizing intergalactic game of future played between humans and aliens at the giant extraterrestrial ship hovering in the sky over Earth. A young man with enormous power of an unknown nature joins the team of hot-headed superheroes in exchange for a cure for his mother’s deadly illness. The Four from Earth will fight not only to defend the honor of their home planet in the spectacular game, but to face the unprecedented threat to the Galaxy and embrace their own destiny.",
            "popularity": 1795.964,
            "poster_path": "/bSpmhdaslwYH2fn2mj7cRcrN5Vi.jpg",
            "release_date": "2020-08-27",
            "title": "Cosmoball",
            "video": false,
            "vote_average": 4.7,
            "vote_count": 21
        },
        {
            "adult": false,
            "backdrop_path": "/dueiWzWc81UAgnbDAyH4Gjqnh4n.jpg",
            "genre_ids": [
                18,
                878
            ],
            "id": 614911,
            "original_language": "en",
            "original_title": "The Midnight Sky",
            "overview": "A lone scientist in the Arctic races to contact a crew of astronauts returning home to a mysterious global catastrophe.",
            "popularity": 1763.152,
            "poster_path": "/51JxCk77ZCqLzbLkrDl9Qho6KUh.jpg",
            "release_date": "2020-12-10",
            "title": "The Midnight Sky",
            "video": false,
            "vote_average": 6,
            "vote_count": 614
        },
        {
            "adult": false,
            "backdrop_path": "/z15NpieRw7jL7bKoICwLO5j7FgZ.jpg",
            "genre_ids": [
                878
            ],
            "id": 733317,
            "original_language": "en",
            "original_title": "Monsters of Man",
            "overview": "A robotics company vying to win a lucrative military contract team up with a corrupt CIA agent to conduct an illegal live field test. They deploy four weaponized prototype robots into a suspected drug manufacturing camp in the Golden Triangle, assuming they'd be killing drug runners that no one would miss. Six doctors on a humanitarian mission witness the brutal slaughter and become prime targets.",
            "popularity": 1582.855,
            "poster_path": "/1f3qspv64L5FXrRy0MF8X92ieuw.jpg",
            "release_date": "2020-11-19",
            "title": "Monsters of Man",
            "video": false,
            "vote_average": 7.2,
            "vote_count": 103
        },
        {
            "adult": false,
            "backdrop_path": "/jIJfDVGKM8GXgpJc2XH2x7e11wm.jpg",
            "genre_ids": [
                28,
                53,
                80,
                18
            ],
            "id": 553604,
            "original_language": "en",
            "original_title": "Honest Thief",
            "overview": "A bank robber tries to turn himself in because he's falling in love and wants to live an honest life...but when he realizes the Feds are more corrupt than him, he must fight back to clear his name.",
            "popularity": 1563.956,
            "poster_path": "/zeD4PabP6099gpE0STWJrJrCBCs.jpg",
            "release_date": "2020-09-03",
            "title": "Honest Thief",
            "video": false,
            "vote_average": 6.8,
            "vote_count": 345
        },
        {
            "adult": false,
            "backdrop_path": "/wzJRB4MKi3yK138bJyuL9nx47y6.jpg",
            "genre_ids": [
                28,
                53,
                878
            ],
            "id": 577922,
            "original_language": "en",
            "original_title": "Tenet",
            "overview": "Armed with only one word - Tenet - and fighting for the survival of the entire world, the Protagonist journeys through a twilight world of international espionage on a mission that will unfold in something beyond real time.",
            "popularity": 1424.415,
            "poster_path": "/k68nPLbIST6NP96JmTxmZijEvCA.jpg",
            "release_date": "2020-08-22",
            "title": "Tenet",
            "video": false,
            "vote_average": 7.4,
            "vote_count": 3471
        },
        {
            "adult": false,
            "backdrop_path": "/2Fk3AB8E9dYIBc2ywJkxk8BTyhc.jpg",
            "genre_ids": [
                28,
                53
            ],
            "id": 524047,
            "original_language": "en",
            "original_title": "Greenland",
            "overview": "John Garrity, his estranged wife and their young son embark on a perilous journey to find sanctuary as a planet-killing comet hurtles toward Earth. Amid terrifying accounts of cities getting levelled, the Garrity's experience the best and worst in humanity. As the countdown to the global apocalypse approaches zero, their incredible trek culminates in a desperate and last-minute flight to a possible safe haven.",
            "popularity": 1236.23,
            "poster_path": "/bNo2mcvSwIvnx8K6y1euAc1TLVq.jpg",
            "release_date": "2020-07-29",
            "title": "Greenland",
            "video": false,
            "vote_average": 7.1,
            "vote_count": 1163
        },
        {
            "adult": false,
            "backdrop_path": "/wk58aoyWpMTVkKkdjw889XfWGdL.jpg",
            "genre_ids": [
                53,
                80,
                9648
            ],
            "id": 646593,
            "original_language": "en",
            "original_title": "Wander",
            "overview": "After getting hired to probe a suspicious death in the small town of Wander, a mentally unstable private investigator becomes convinced the case is linked to the same 'conspiracy cover up' that caused the death of his daughter.",
            "popularity": 990.699,
            "poster_path": "/2AwPvNHphpZBJDqjZKVuMAbvS0v.jpg",
            "release_date": "2020-12-04",
            "title": "Wander",
            "video": false,
            "vote_average": 5.5,
            "vote_count": 51
        },
        {
            "adult": false,
            "backdrop_path": "/jeAQdDX9nguP6YOX6QSWKDPkbBo.jpg",
            "genre_ids": [
                28,
                14,
                878
            ],
            "id": 590706,
            "original_language": "en",
            "original_title": "Jiu Jitsu",
            "overview": "Every six years, an ancient order of jiu-jitsu fighters joins forces to battle a vicious race of alien invaders. But when a celebrated war hero goes down in defeat, the fate of the planet and mankind hangs in the balance.",
            "popularity": 984.755,
            "poster_path": "/eLT8Cu357VOwBVTitkmlDEg32Fs.jpg",
            "release_date": "2020-11-20",
            "title": "Jiu Jitsu",
            "video": false,
            "vote_average": 5.4,
            "vote_count": 218
        },
        {
            "adult": false,
            "backdrop_path": "/ckfwfLkl0CkafTasoRw5FILhZAS.jpg",
            "genre_ids": [
                28,
                35,
                14
            ],
            "id": 602211,
            "original_language": "en",
            "original_title": "Fatman",
            "overview": "A rowdy, unorthodox Santa Claus is fighting to save his declining business. Meanwhile, Billy, a neglected and precocious 12 year old, hires a hit man to kill Santa after receiving a lump of coal in his stocking.",
            "popularity": 901.878,
            "poster_path": "/4n8QNNdk4BOX9Dslfbz5Dy6j1HK.jpg",
            "release_date": "2020-11-13",
            "title": "Fatman",
            "video": false,
            "vote_average": 5.7,
            "vote_count": 277
        },
        {
            "adult": false,
            "backdrop_path": "/yR27bZPIkNhpGEIP3jKV2EifTgo.jpg",
            "genre_ids": [
                16,
                10751
            ],
            "id": 755812,
            "original_language": "fr",
            "original_title": "Miraculous World: New York, United HeroeZ",
            "overview": "During a school field trip, Ladybug and Cat Noir meet the American superheroes, whom they have to save from an akumatised super-villain. They discover that Miraculous exist in the United States too.",
            "popularity": 896.435,
            "poster_path": "/kIHgjAkuzvKBnmdstpBOo4AfZah.jpg",
            "release_date": "2020-10-10",
            "title": "Miraculous World: New York, United HeroeZ",
            "video": false,
            "vote_average": 8.6,
            "vote_count": 182
        },
        {
            "adult": false,
            "backdrop_path": "/UgNke0mMQhQdnX2hEu4cN83M0a.jpg",
            "genre_ids": [
                28,
                14,
                10751
            ],
            "id": 615677,
            "original_language": "en",
            "original_title": "We Can Be Heroes",
            "overview": "When alien invaders capture Earth's superheroes, their kids must learn to work together to save their parents - and the planet.",
            "popularity": 778.183,
            "poster_path": "/1S21HpcKY6uQ9UAw68aICmrJaq6.jpg",
            "release_date": "2020-12-25",
            "title": "We Can Be Heroes",
            "video": false,
            "vote_average": 6.2,
            "vote_count": 120
        },
        {
            "adult": false,
            "backdrop_path": "/azTsf28VYj0rw6ql67fB8WnxbZX.jpg",
            "genre_ids": [
                53,
                27
            ],
            "id": 554585,
            "original_language": "es",
            "original_title": "Sin origen",
            "overview": "A group of arcanes enjoy fighting vampires.",
            "popularity": 777.726,
            "poster_path": "/77lI0btz8qkExY13MKXaPrf4cA6.jpg",
            "release_date": "2020-10-25",
            "title": "Originless",
            "video": false,
            "vote_average": 5.4,
            "vote_count": 30
        },
        {
            "adult": false,
            "backdrop_path": "/lrzAb9QCwKPOwH1iY7WpfjR6jcC.jpg",
            "genre_ids": [
                10770,
                53
            ],
            "id": 648371,
            "original_language": "en",
            "original_title": "No Good Deed",
            "overview": "Karen never planned on being a hero. A recent widow, she has her hands full with work and parenting her son Max. Then Karen saves Jeremy's life during a drug store robbery and quickly discovers that the young man is intent on paying her back at any cost. At first, life starts improving for this good Samaritan, but as Jeremy's efforts become more extreme, Karen starts to wonder if no good deed truly goes unpunished.",
            "popularity": 757.067,
            "poster_path": "/27BnREBR5rcMrUVUhV5bTXlcCym.jpg",
            "release_date": "2020-03-13",
            "title": "No Good Deed",
            "video": false,
            "vote_average": 5.9,
            "vote_count": 18
        },
        {
            "adult": false,
            "backdrop_path": "/umrpNeJooAyfvgPuOutNQHZdJ2p.jpg",
            "genre_ids": [
                35,
                18
            ],
            "id": 652962,
            "original_language": "en",
            "original_title": "Half Brothers",
            "overview": "A story about the complex connection with a brother who is based in Mexico, meant to be a metaphor of the relationship between neighboring countries America and Mexico.",
            "popularity": 712.072,
            "poster_path": "/Apl0WFx61trVOoxvc8Erd5cbP8X.jpg",
            "release_date": "2020-12-04",
            "title": "Half Brothers",
            "video": false,
            "vote_average": 4.9,
            "vote_count": 7
        },
        {
            "adult": false,
            "backdrop_path": "/8knaRrDd1FM1pbSLaViEQSxodi5.jpg",
            "genre_ids": [
                18,
                53
            ],
            "id": 441282,
            "original_language": "en",
            "original_title": "Night Hunter",
            "overview": "A Minnesota police officer crosses paths with a committed and tireless vigilante as he follows the trail of a ruthless predator responsible for several abductions and murders.",
            "popularity": 705.289,
            "poster_path": "/vVYU0x9FRpiJNX7c54ciFnRBVYG.jpg",
            "release_date": "2019-08-29",
            "title": "Night Hunter",
            "video": false,
            "vote_average": 6.5,
            "vote_count": 405
        },
        {
            "adult": false,
            "backdrop_path": "/d2UxKyaJ5GgzuHaSsWinFfv3g6L.jpg",
            "genre_ids": [
                28,
                27,
                53
            ],
            "id": 581392,
            "original_language": "ko",
            "original_title": "반도",
            "overview": "A soldier and his team battle hordes of post-apocalyptic zombies in the wastelands of the Korean Peninsula.",
            "popularity": 683.698,
            "poster_path": "/sy6DvAu72kjoseZEjocnm2ZZ09i.jpg",
            "release_date": "2020-07-15",
            "title": "Peninsula",
            "video": false,
            "vote_average": 6.9,
            "vote_count": 1007
        },
        {
            "adult": false,
            "backdrop_path": "/tfxRXujXbi2esl8WLhdqzixjbIY.jpg",
            "genre_ids": [],
            "id": 669444,
            "original_language": "fr",
            "original_title": "Les chevaliers du fiel dynamitent 2019",
            "overview": "",
            "popularity": 682.25,
            "poster_path": "/oh6AZL0nnQdnz8bazoDzTAVVHfB.jpg",
            "release_date": "",
            "title": "Les chevaliers du fiel dynamitent 2019",
            "video": false,
            "vote_average": 4.9,
            "vote_count": 4
        }
    ],
    "total_pages": 500,
    "total_results": 10000
}

Создание шаблона

Шаблон создаётся на вкладке Tests или Pre-request Script. Шаблон — это строка в формате Handlebars. Шаблонизатор Handlebars имеет большой список возможностей, но для примера будем пользоваться только некоторыми из них. За подробной документацией обращайтесь на сайт Handlebars.

Шаблон применяется при вызове метода pm.visualizer.set. Метод принимает три параметра:

  1. layout (обязательный параметр) — Handlebar-шаблон в виде строки,
  2. data (опциональный параметр) — данные, которые применяются к шаблону,
  3. options (опциональный параметр) — параметры компилятора Handlebars-шаблонов.

Первый шаблон

Для начала сделаем простой шаблон. В этом представлении фильмы будут отображаться в виде таблицы. Для этого достаточно базовой функциональности шаблонизатора и начальных навыков в HTML.

Добавьте на вкладку Tests или Pre-request Script следущий сниппет:

let table_template = `
<html>
    <body>
        <table>
            <thead>
                <tr>
                    <td>Original Title</td>
                    <td>Original Language</td>
                    <td>Overview</td>
                    <td>Avg Vote</td>
                </tr>
            </thead>
            {{#each response.results}}
                <tr>
                    <td>{{original_title}}</td>
                    <td>{{original_language}}</td>
                    <td>{{overview}}</td>
                    <td>{{vote_average}}</td>
                </tr>
            {{/each}}    
        </table>
    </body>
</html>
`;

pm.visualizer.set(table_template, { response: pm.response.json() });

Обратите внимание на две вещи:

  1. Фигурные скобки в шаблоне — это маркеры. Маркеры заменяются одноимёнными полями из данных, которые передали вторым аргументом в метод pm.visualizer.set,
  2. Второй аргумент может быть любыми данными. Например, можно взять JSON, который приведён выше и передать его вместо pm.response.json() или добавить дополнительные данные. Например, из переменных окружения.

Конструкция {{#each}} из Handlebars позволяет применить к каждому элементу коллекции шаблон расположенный внутри блока each.

В результате будет сгенерирован следующий вывод в представлении Visualize на вкладке Body ответа:

table-view

Шаблон на стероидах

Сделаем шаблон, в котором фильмы будут представлены в виде карточек — так гораздо нагляднее.

В шаблонах можно использовать сторонние JavaScript или CSS библиотеки. Для демонстрации этого добавим ссылку на Bootstrap и воспользуемся его функциональностью для создания представления с карточками:


let template = `
<html>
<body>
    <head>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
        <style>
        .card { border-radius: 8px, max-height: 100px; }
        </style>
    </head>
    <h1>Request to "{{query}}"</h1>
    <div class='row'>
        {{#each response.results}}
        <div class='col'>
            <div class="card" style="width: 18rem;">
                <img src="https://image.tmdb.org/t/p/w220_and_h330_face/{{poster_path}}" class="card-img-top" alt="{{original_title}}">
                <div class="card-body">
                    <h5 class="card-title">{{original_title}}</h5>
                    <p class="card-text">{{overview}}</p>
                    <p>Genres:
                    {{#each genre_ids}}
                    <span class='badge bg-secondary'>{{this}}</span>
                    {{/each}}
                    </p>
                    <a href="https://www.themoviedb.org/movie/{{id}}" class="btn btn-primary">Open on themoviedb.com</a>
                </div>
            </div>
        </div>
        {{/each}}
    </div>
</body>
</html>
`;

pm.visualizer.set(template, { response: pm.response.json(), query: pm.request.url.getPath() });

Изменений в шаблоне немного, но разница в результате заметна невооружённым глазом:

cards-view

Отладка шаблонов

Так как Postman под капотом — браузер, то отлаживать отображение просто. Для этого можно кликнуть правой кнопкой мыши по отрендеренному представлению и выбрать пункт Inspect Visualization. При этом откроется инструменты разработки Chromium-а:

cards-view

Здесь можно экспериментировать со стилями и разметкой.

На этом всё. Если возникнут вопросы — задавайте в комментариях. Если смогу, то отвечу.

PS: С недавних пор блог обзавёлся каналом в telegram. Пишу не часто, но о том, что показалось интересным из мира .NET, Python, Go. Присоединяйтесь по ссылке.