1 of 22

Fetching Data from APIs using Mojolicious

Boyd Duffee, LPW 2025

2 of 22

Getting Started

tl;dr - just go out and buy Mojolicious Web Clients by brian d foy and follow your nose.

… the second half talks about testing your client!

Today I’m talking about REST, not SOAP. That’s a story for another time.

3 of 22

Live Now on CPAN

Astro::ADS

WebService::OurWorldInData

Thank you!

CPAN Testing Service for finding all my install fails.

No Dev key found …

Dev key in file not the environment …

Etc, etc…

4 of 22

Write a Base Class to hold the attributes every http request uses:

  • base_urlThe server for the API + base path.
  • uaFor the UserAgent (which we mock in tests)
  • tokenYour dev key?
  • proxyWill you need one?

1a. Scafolding

5 of 22

Write wrapper functions around your REST calls so you Don’t Repeat Yourself. This gives consistency to your interface.

  • AuthenticationAdd your API key to the Authentication headers on every request.
  • Error handlingDo you warn, die or return a bespoke Error object?
  • DebuggingIt worked yesterday. WTF happened today?

1b. Scafolding

6 of 22

my $tx = $self->ua->build_tx( GET => $url );

$tx->req->headers->authorization( 'Bearer ' . $self->token );

warn $tx->req->to_string if $DEBUG; #optional

Tip

To add headers, you need to build the transaction object $tx yourself.

Using built-in get() or post() sends the request immediately.

Authorization

7 of 22

my $tx = $self->ua->build_tx( GET => $url );

$tx->req->headers->authorization( 'Bearer ' . $self->token );

try { $tx = $self->ua->start($tx) }

catch ($error) {

carp "Got this error: ", $error;

}

# suppress warning for native perl 5.36 try/catch

no if $] >= 5.018, 'warnings', 'experimental';

Tip

$self->ua->start($tx) tells the UserAgent to make the request.

Feature::Compat::Try; gives you the new try/catch syntax on Perls older than v5.36

Error handling

8 of 22

The response to your API call is a result, but not always provided in the desired format.

  • Result�A class to contain the answer to your API call which can implement any follow-up methods i.e. next_page()
  • MappingI like Moo::Role for mapping the response to my Result class.

2. Results

9 of 22

package MyClient::Role::ResultMapper;

use Moo::Role;

use strictures 2;

use MyClient;

sub parse_response {

my ($self, $json) = @_;

return unless exists $json->{responseHeader};

@{$result_params}{ qw<q rows> } =

@{$json->{response}{params}}{ qw<q rows> };

Tip

strictures 2 turns on strict and makes most warnings fatal.

Oooh, here’s a lovely hash slice mapping values 2 levels up from the json response

MyClient::Role::ResultMapper

10 of 22

The internet is not always with us, so use a module that listens to the HTTP traffic, record the conversation and play back the responses instead of constantly hammering the target website:

  • LWP::UserAgent::Mockable�Thank you, Michael Jemmeson! Good docs for getting started.
  • Mojo::UserAgent::MockableThis is the one I use out of habit. It’s compatible with the first module.

3. Testing

11 of 22

package Test::MyClient;

# set env variable for playback only

$ENV{DEV_KEY} = 'A_very_long_string'

unless $ENV{LWP_UA_MOCK} eq 'record'

|| $ENV{LWP_UA_MOCK} eq 'passthrough';

1;

Tip

Use the real DEV_KEY for talking to the live service.

Also a good place for start-up and tear-down methods to make your tests consistent.

t/lib/Test/MyClient.pm

12 of 22

use lib 't/lib';

use Test::MyClient;

BEGIN {

$ENV{ LWP_UA_MOCK } ||= 'playback';

$ENV{ LWP_UA_MOCK_FILE } ||= __FILE__.'-lwp-mock.out';

}

...

Tip

This snippet is from the docs on LWP::UserAgent::Mockable which controls behaviour with environment variables instead of code.

t/endpoint.t

13 of 22

...

skip_all('No API key found in test suite')

unless $ENV{DEV_KEY};

my $ua = Mojo::UserAgent::Mockable->new(

mode => 'lwp-ua-mockable',

ignore_headers => 'all'

);

my $client = MyClient->new( q => 'search_term',

ua => $ua

);

Tip

The skip_all turns a CPANTS fail report into an unknown report

LWP mode checks environment variables

Ignore the headers

Use mocked UserAgent in tests!

t/endpoint.t

14 of 22

Protect your users from the network, but not from themselves. Pass the API errors through for sensible debugging.

  • Malformed requestsEasy. You made the request, you can mangle it.
  • Network errorsHarder. The API should tell you what each error code means, i.e. 409 Service Unavailable only happens during maintenance.
  • TimeoutsGood luck. Let me know if you do it.

4. Error Testing

15 of 22

use Test2::Tools::Exception;

subtest 'Bad Key - Authorisation failure' => sub {

local $ENV{SECRET_DEV_KEY} = 'BAD';

like(

warning { $client->method() },

qr/HTTP Error: $expected_error/,

'Caught dev token error'

);

Tip

Make a single test file for each hard-to-reproduce network error.

Record when you can, playback at leisure.

t/useragent.t

16 of 22

Scrub your DEV_KEY with a

Git pre-commit hook

before committing it to the repository

Security Tip

Do Not commit your dev key to a public repository.

Recording your mocks saved the http headers in the mock file – which included the Authorization.

Ooops!

17 of 22

s/Bearer \w{10,}/

Bearer TOKEN_REMOVED/g

Tip

My auth header starts with Bearer and my 40 character dev_key.

Image: https://xkcd.com/208

18 of 22

use v5.20;

use FindBin qw($Bin);

use File::Find;

use Mojo::File;

# for non-destructive testing

my $sub = sub {

my $name = $File::Find::name;

say $name if $name =~ /out$/;

};

...

Tip

Find all recorded mocks.

Save this script in

.git/hooks/pre-commit

scrub_mock_headers.pl

19 of 22

...

my $scrub = sub {

next unless $File::Find::name =~ /mock\.out$/;

my $path = Mojo::File->new( $File::Find::name );

my $mock = $path->slurp;

say 'Scrubbing ', $path->basename

if $mock =~ s/Bearer \w{10,}/Bearer TOKEN_REMOVED/g;

$path->spew( $mock )

or warn "Error writing to $path: $!\n";

};

find $scrub, "$Bin/../t";

And saved as .git/hooks/pre-commit

20 of 22

Software engineers,

ask me

How long does it take

To write an endpoint?

21 of 22

Working examples at

https://github.com/duffee

/astro-ads

/perl-OurWorldInData

22 of 22

Thank You

You have been enthralled and entertained by

Boyd Duffee

I am DUFFEE on CPAN, duffee on github, gitlab and dev.to

I have started to write sciency things at

https://perldatascience.wordpress.com/

And yes, I am currently looking for work ;)

boyd.duffee@gmail.com