# Chapter 21: RESTful APIs and Web Services *"In the interconnected world of modern software, Perl serves as both a capable API consumer and a powerful service provider."* ## The Web Services Landscape In 2025, RESTful APIs remain the lingua franca of web services. While GraphQL and gRPC have their niches, REST's simplicity and ubiquity make it essential for any systems programmer. Perl, with its excellent HTTP libraries and JSON handling, excels at both consuming and providing web services. ## Building a RESTful API Server Let's start by building a complete REST API using Mojolicious, Perl's most modern web framework: ```perl #!/usr/bin/env perl use Mojolicious::Lite -signatures; use Mojo::JSON qw(encode_json decode_json); use DBI; use Try::Tiny; # Initialize database my $dbh = DBI->connect("dbi:SQLite:dbname=api.db","","", { RaiseError => 1, PrintError => 0, AutoCommit => 1, }); # Create tables if needed $dbh->do(q{ CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, email TEXT UNIQUE NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) }); $dbh->do(q{ CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, title TEXT NOT NULL, content TEXT, published BOOLEAN DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ) }); # Middleware for JSON error handling hook before_dispatch => sub ($c) { $c->res->headers->header('Content-Type' => 'application/json'); }; hook after_dispatch => sub ($c) { if ($c->res->code >= 400 && !$c->res->body) { $c->render(json => { error => $c->res->message || 'An error occurred', status => $c->res->code }); } }; # Helper for database operations helper db => sub { $dbh }; # Helper for pagination helper paginate => sub ($c, $query, $count_query, @params) { my $page = $c->param('page') // 1; my $limit = $c->param('limit') // 10; my $offset = ($page - 1) * $limit; # Get total count my ($total) = $dbh->selectrow_array($count_query, {}, @params); # Get paginated results my $results = $dbh->selectall_arrayref( "$query LIMIT ? OFFSET ?", { Slice => {} }, @params, $limit, $offset ); return { data => $results, meta => { page => $page, limit => $limit, total => $total, pages => int(($total + $limit - 1) / $limit) } }; }; # Routes # Health check endpoint get '/health' => sub ($c) { $c->render(json => { status => 'healthy', timestamp => time(), version => '1.0.0' }); }; # User endpoints group { under '/api/v1'; # List users with pagination get '/users' => sub ($c) { my $result = $c->paginate( "SELECT * FROM users ORDER BY created_at DESC", "SELECT COUNT(*) FROM users" ); $c->render(json => $result); }; # Get single user get '/users/:id' => sub ($c) { my $id = $c->param('id'); my $user = $dbh->selectrow_hashref( "SELECT * FROM users WHERE id = ?", {}, $id ); return $c->render(json => { error => 'User not found' }, status => 404) unless $user; # Get user's posts $user->{posts} = $dbh->selectall_arrayref( "SELECT id, title, published FROM posts WHERE user_id = ?", { Slice => {} }, $id ); $c->render(json => { data => $user }); }; # Create user post '/users' => sub ($c) { my $json = $c->req->json; # Validation my @required = qw(username email); for my $field (@required) { return $c->render( json => { error => "Missing required field: $field" }, status => 400 ) unless $json->{$field}; } # Check for duplicates my $existing = $dbh->selectrow_hashref( "SELECT id FROM users WHERE username = ? OR email = ?", {}, $json->{username}, $json->{email} ); return $c->render( json => { error => 'Username or email already exists' }, status => 409 ) if $existing; # Insert user try { $dbh->do( "INSERT INTO users (username, email) VALUES (?, ?)", {}, $json->{username}, $json->{email} ); my $id = $dbh->last_insert_id(undef, undef, 'users', undef); my $user = $dbh->selectrow_hashref( "SELECT * FROM users WHERE id = ?", {}, $id ); $c->render(json => { data => $user }, status => 201); } catch { $c->render(json => { error => "Failed to create user: $_" }, status => 500); }; }; # Update user put '/users/:id' => sub ($c) { my $id = $c->param('id'); my $json = $c->req->json; # Check if user exists my $user = $dbh->selectrow_hashref( "SELECT * FROM users WHERE id = ?", {}, $id ); return $c->render(json => { error => 'User not found' }, status => 404) unless $user; # Build update query my @fields; my @values; for my $field (qw(username email)) { if (exists $json->{$field}) { push @fields, "$field = ?"; push @values, $json->{$field}; } } return $c->render(json => { error => 'No fields to update' }, status => 400) unless @fields; push @values, $id; try { $dbh->do( "UPDATE users SET " . join(', ', @fields) . ", updated_at = CURRENT_TIMESTAMP WHERE id = ?", {}, @values ); $user = $dbh->selectrow_hashref( "SELECT * FROM users WHERE id = ?", {}, $id ); $c->render(json => { data => $user }); } catch { $c->render(json => { error => "Failed to update user: $_" }, status => 500); }; }; # Delete user del '/users/:id' => sub ($c) { my $id = $c->param('id'); my $user = $dbh->selectrow_hashref( "SELECT * FROM users WHERE id = ?", {}, $id ); return $c->render(json => { error => 'User not found' }, status => 404) unless $user; try { $dbh->begin_work; # Delete user's posts first $dbh->do("DELETE FROM posts WHERE user_id = ?", {}, $id); # Delete user $dbh->do("DELETE FROM users WHERE id = ?", {}, $id); $dbh->commit; $c->render(json => { message => 'User deleted successfully' }); } catch { $dbh->rollback; $c->render(json => { error => "Failed to delete user: $_" }, status => 500); }; }; # Post endpoints # List all posts get '/posts' => sub ($c) { my $published_only = $c->param('published') // 0; my $where = $published_only ? "WHERE published = 1" : ""; my $result = $c->paginate( "SELECT p.*, u.username FROM posts p JOIN users u ON p.user_id = u.id $where ORDER BY p.created_at DESC", "SELECT COUNT(*) FROM posts p $where" ); $c->render(json => $result); }; # Create post post '/posts' => sub ($c) { my $json = $c->req->json; # Validation my @required = qw(user_id title content); for my $field (@required) { return $c->render( json => { error => "Missing required field: $field" }, status => 400 ) unless defined $json->{$field}; } # Check if user exists my $user = $dbh->selectrow_hashref( "SELECT id FROM users WHERE id = ?", {}, $json->{user_id} ); return $c->render(json => { error => 'User not found' }, status => 404) unless $user; try { $dbh->do( "INSERT INTO posts (user_id, title, content, published) VALUES (?, ?, ?, ?)", {}, $json->{user_id}, $json->{title}, $json->{content}, $json->{published} // 0 ); my $id = $dbh->last_insert_id(undef, undef, 'posts', undef); my $post = $dbh->selectrow_hashref( "SELECT p.*, u.username FROM posts p JOIN users u ON p.user_id = u.id WHERE p.id = ?", {}, $id ); $c->render(json => { data => $post }, status => 201); } catch { $c->render(json => { error => "Failed to create post: $_" }, status => 500); }; }; # Search posts get '/posts/search' => sub ($c) { my $query = $c->param('q'); return $c->render(json => { error => 'Query parameter required' }, status => 400) unless $query; my $posts = $dbh->selectall_arrayref( "SELECT p.*, u.username FROM posts p JOIN users u ON p.user_id = u.id WHERE p.title LIKE ? OR p.content LIKE ? ORDER BY p.created_at DESC LIMIT 20", { Slice => {} }, "%$query%", "%$query%" ); $c->render(json => { data => $posts }); }; }; # Start the application app->start; ``` ## Consuming External APIs Now let's build a comprehensive API client that demonstrates best practices: ```perl #!/usr/bin/env perl use Modern::Perl '2018'; use feature 'signatures'; no warnings 'experimental::signatures'; package API::Client { use Moo; use Types::Standard qw(Str Int HashRef Bool); use LWP::UserAgent; use HTTP::Request; use JSON::XS; use URI; use Try::Tiny; use Time::HiRes qw(time); use Cache::LRU; use Log::Any qw($log); has base_url => ( is => 'ro', isa => Str, required => 1, ); has api_key => ( is => 'ro', isa => Str, predicate => 'has_api_key', ); has timeout => ( is => 'ro', isa => Int, default => 30, ); has retry_count => ( is => 'ro', isa => Int, default => 3, ); has retry_delay => ( is => 'ro', isa => Int, default => 1, ); has cache_ttl => ( is => 'ro', isa => Int, default => 300, # 5 minutes ); has user_agent => ( is => 'lazy', isa => sub { ref $_[0] eq 'LWP::UserAgent' }, ); has json => ( is => 'lazy', default => sub { JSON::XS->new->utf8->pretty->canonical }, ); has cache => ( is => 'lazy', default => sub { Cache::LRU->new(size => 100) }, ); has _cache_timestamps => ( is => 'ro', default => sub { {} }, ); sub _build_user_agent ($self) { my $ua = LWP::UserAgent->new( timeout => $self->timeout, agent => 'Perl API Client/1.0', ); # Add default headers $ua->default_header('Accept' => 'application/json'); $ua->default_header('Content-Type' => 'application/json'); # Add API key if provided if ($self->has_api_key) { $ua->default_header('Authorization' => 'Bearer ' . $self->api_key); } return $ua; } sub request ($self, $method, $endpoint, $params = {}, $body = undef) { # Build full URL my $uri = URI->new($self->base_url . $endpoint); # Add query parameters for GET requests if ($method eq 'GET' && $params && %$params) { $uri->query_form($params); } # Check cache for GET requests my $cache_key = "$method:$uri"; if ($method eq 'GET') { my $cached = $self->_get_cached($cache_key); return $cached if $cached; } # Prepare request my $request = HTTP::Request->new($method => $uri); # Add body for POST/PUT/PATCH requests if ($body && ($method eq 'POST' || $method eq 'PUT' || $method eq 'PATCH')) { my $json_body = ref $body ? $self->json->encode($body) : $body; $request->content($json_body); } # Execute request with retries my $response; my $attempt = 0; my $last_error; while ($attempt < $self->retry_count) { $attempt++; $log->debug("API request attempt $attempt: $method $uri"); my $start_time = time(); try { $response = $self->user_agent->request($request); my $elapsed = sprintf("%.3f", time() - $start_time); $log->debug("API response: " . $response->code . " (${elapsed}s)"); # Check for rate limiting if ($response->code == 429) { my $retry_after = $response->header('Retry-After') || $self->retry_delay * $attempt; $log->warn("Rate limited, waiting ${retry_after}s"); sleep($retry_after); next; } # Success or client error (no retry needed) if ($response->is_success || ($response->code >= 400 && $response->code < 500)) { last; } # Server error - retry if ($response->code >= 500) { $last_error = "Server error: " . $response->status_line; $log->warn($last_error); sleep($self->retry_delay * $attempt) if $attempt < $self->retry_count; next; } } catch { $last_error = "Request failed: $_"; $log->error($last_error); sleep($self->retry_delay * $attempt) if $attempt < $self->retry_count; }; } # Check final response unless ($response) { die "API request failed after $attempt attempts: $last_error"; } unless ($response->is_success) { my $error = $response->status_line; if ($response->content) { try { my $error_data = $self->json->decode($response->content); $error = $error_data->{error} || $error_data->{message} || $error; } catch { # Use status line if JSON decode fails }; } die "API error: $error"; } # Parse response my $data; if ($response->content) { try { $data = $self->json->decode($response->content); } catch { die "Failed to parse API response: $_"; }; } # Cache successful GET requests if ($method eq 'GET' && $data) { $self->_set_cached($cache_key, $data); } return $data; } sub _get_cached ($self, $key) { my $timestamp = $self->_cache_timestamps->{$key}; return unless $timestamp; if (time() - $timestamp > $self->cache_ttl) { delete $self->_cache_timestamps->{$key}; $self->cache->remove($key); return; } return $self->cache->get($key); } sub _set_cached ($self, $key, $value) { $self->cache->set($key => $value); $self->_cache_timestamps->{$key} = time(); } # Convenience methods sub get ($self, $endpoint, $params = {}) { return $self->request('GET', $endpoint, $params); } sub post ($self, $endpoint, $body = {}, $params = {}) { return $self->request('POST', $endpoint, $params, $body); } sub put ($self, $endpoint, $body = {}, $params = {}) { return $self->request('PUT', $endpoint, $params, $body); } sub patch ($self, $endpoint, $body = {}, $params = {}) { return $self->request('PATCH', $endpoint, $params, $body); } sub delete ($self, $endpoint, $params = {}) { return $self->request('DELETE', $endpoint, $params); } } # Example: GitHub API client package GitHub::Client { use Moo; use Types::Standard qw(Str); extends 'API::Client'; has '+base_url' => ( default => 'https://api.github.com', ); has username => ( is => 'ro', isa => Str, required => 1, ); sub user_repos ($self) { return $self->get("/users/" . $self->username . "/repos", { sort => 'updated', per_page => 100, }); } sub repo_info ($self, $repo) { return $self->get("/repos/" . $self->username . "/$repo"); } sub repo_issues ($self, $repo, $state = 'open') { return $self->get("/repos/" . $self->username . "/$repo/issues", { state => $state, per_page => 100, }); } sub create_issue ($self, $repo, $title, $body, $labels = []) { return $self->post("/repos/" . $self->username . "/$repo/issues", { title => $title, body => $body, labels => $labels, }); } } # Usage example use Log::Any::Adapter 'Stdout'; my $github = GitHub::Client->new( username => 'torvalds', api_key => $ENV{GITHUB_TOKEN}, # Optional ); try { # Get user's repositories my $repos = $github->user_repos(); say "Found " . scalar(@$repos) . " repositories"; for my $repo (@$repos) { printf("%-30s ⭐ %-6d 🍴 %-6d %s\n", $repo->{name}, $repo->{stargazers_count}, $repo->{forks_count}, $repo->{description} // '' ); } # Get specific repo details my $linux = $github->repo_info('linux'); say "\nLinux kernel repo:"; say " Stars: " . $linux->{stargazers_count}; say " Forks: " . $linux->{forks_count}; say " Open Issues: " . $linux->{open_issues_count}; } catch { warn "Error: $_"; }; ``` ## WebSocket Communication For real-time applications, WebSocket support is essential: ```perl #!/usr/bin/env perl use Mojolicious::Lite -signatures; use Mojo::JSON qw(encode_json decode_json); # Store active connections my %clients; my $client_id = 0; # WebSocket chat server websocket '/ws' => sub ($c) { my $id = ++$client_id; # Store connection $clients{$id} = $c; # Send welcome message $c->send(encode_json({ type => 'system', message => "Welcome! You are client #$id", timestamp => time(), })); # Notify others of new connection broadcast({ type => 'system', message => "Client #$id joined", timestamp => time(), }, $id); # Handle incoming messages $c->on(message => sub ($c, $msg) { my $data; # Try to parse JSON eval { $data = decode_json($msg) }; if ($@) { $c->send(encode_json({ type => 'error', message => 'Invalid JSON', })); return; } # Handle different message types if ($data->{type} eq 'chat') { broadcast({ type => 'chat', from => "Client #$id", message => $data->{message}, timestamp => time(), }); } elsif ($data->{type} eq 'ping') { $c->send(encode_json({ type => 'pong', timestamp => time(), })); } }); # Handle disconnection $c->on(finish => sub ($c, $code, $reason) { delete $clients{$id}; broadcast({ type => 'system', message => "Client #$id disconnected", timestamp => time(), }); }); }; sub broadcast ($message, $exclude_id = undef) { my $json = encode_json($message); for my $id (keys %clients) { next if defined $exclude_id && $id == $exclude_id; $clients{$id}->send($json); } } # Serve HTML client get '/' => sub ($c) { $c->render(inline => <<'HTML'); WebSocket Chat

WebSocket Chat

HTML }; app->start; ``` ## GraphQL Integration While REST dominates, GraphQL offers powerful query capabilities: ```perl #!/usr/bin/env perl use Modern::Perl '2018'; use feature 'signatures'; no warnings 'experimental::signatures'; package GraphQL::Client { use Moo; use Types::Standard qw(Str HashRef); use LWP::UserAgent; use HTTP::Request; use JSON::XS; use Try::Tiny; has endpoint => ( is => 'ro', isa => Str, required => 1, ); has headers => ( is => 'ro', isa => HashRef, default => sub { {} }, ); has ua => ( is => 'lazy', default => sub { my $ua = LWP::UserAgent->new(timeout => 30); $ua->default_header('Content-Type' => 'application/json'); return $ua; }, ); has json => ( is => 'lazy', default => sub { JSON::XS->new->utf8->pretty }, ); sub query ($self, $query, $variables = {}, $operation_name = undef) { my $request_body = { query => $query, variables => $variables, }; $request_body->{operationName} = $operation_name if $operation_name; my $request = HTTP::Request->new('POST' => $self->endpoint); $request->content($self->json->encode($request_body)); # Add custom headers for my $header (keys %{$self->headers}) { $request->header($header => $self->headers->{$header}); } my $response = $self->ua->request($request); unless ($response->is_success) { die "GraphQL request failed: " . $response->status_line; } my $data = try { $self->json->decode($response->content); } catch { die "Failed to parse GraphQL response: $_"; }; if ($data->{errors}) { my $errors = join(", ", map { $_->{message} } @{$data->{errors}}); die "GraphQL errors: $errors"; } return $data->{data}; } sub introspect ($self) { my $introspection_query = q{ query IntrospectionQuery { __schema { types { name kind description fields { name type { name kind } } } } } }; return $self->query($introspection_query); } } # Example: GitHub GraphQL API my $github_graphql = GraphQL::Client->new( endpoint => 'https://api.github.com/graphql', headers => { Authorization => "Bearer $ENV{GITHUB_TOKEN}", }, ); # Query repository information my $repo_query = q{ query GetRepository($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { name description stargazerCount forkCount primaryLanguage { name } issues(states: OPEN) { totalCount } pullRequests(states: OPEN) { totalCount } releases(last: 5) { nodes { tagName publishedAt } } } } }; try { my $data = $github_graphql->query($repo_query, { owner => 'perl', name => 'perl5', }); my $repo = $data->{repository}; say "Repository: $repo->{name}"; say "Description: $repo->{description}"; say "Stars: $repo->{stargazerCount}"; say "Forks: $repo->{forkCount}"; say "Language: " . ($repo->{primaryLanguage}{name} // 'Unknown'); say "Open Issues: " . $repo->{issues}{totalCount}; say "Open PRs: " . $repo->{pullRequests}{totalCount}; say "\nRecent Releases:"; for my $release (@{$repo->{releases}{nodes}}) { say " $release->{tagName} - $release->{publishedAt}"; } } catch { warn "Error: $_"; }; ``` ## API Documentation with OpenAPI Generate OpenAPI specifications for your Perl APIs: ```perl #!/usr/bin/env perl use Mojolicious::Lite -signatures; use Mojolicious::Plugin::OpenAPI; # Load OpenAPI specification plugin OpenAPI => { url => "data:///openapi.yaml", route => app->routes->under("/api")->to("auth#check"), }; # Controller actions package MyApp::Controller::Pet; use Mojo::Base 'Mojolicious::Controller', -signatures; sub list ($c) { # Get query parameters with validation from OpenAPI spec my $input = $c->validation->output; my @pets = ( { id => 1, name => "Fluffy", type => "cat" }, { id => 2, name => "Rover", type => "dog" }, ); # Filter by type if specified if ($input->{type}) { @pets = grep { $_->{type} eq $input->{type} } @pets; } $c->render(openapi => \@pets); } sub add ($c) { my $input = $c->validation->output; # Simulate adding pet my $pet = { id => int(rand(1000)), %{$input->{body}}, }; $c->render(openapi => $pet, status => 201); } package MyApp::Controller::Auth; use Mojo::Base 'Mojolicious::Controller', -signatures; sub check ($c) { my $api_key = $c->req->headers->header('X-API-Key'); unless ($api_key && $api_key eq 'secret123') { return $c->render( openapi => { error => 'Unauthorized' }, status => 401 ); } return 1; } app->start; __DATA__ @@ openapi.yaml openapi: 3.0.0 info: title: Pet Store API version: 1.0.0 description: A simple pet store API servers: - url: /api paths: /pets: get: operationId: listPets summary: List all pets x-mojo-to: "pet#list" parameters: - name: type in: query schema: type: string enum: [cat, dog, bird] responses: 200: description: List of pets content: application/json: schema: type: array items: $ref: '#/components/schemas/Pet' post: operationId: addPet summary: Add a new pet x-mojo-to: "pet#add" requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/NewPet' responses: 201: description: Pet created content: application/json: schema: $ref: '#/components/schemas/Pet' components: schemas: Pet: type: object required: - id - name - type properties: id: type: integer name: type: string type: type: string NewPet: type: object required: - name - type properties: name: type: string type: type: string securitySchemes: ApiKey: type: apiKey in: header name: X-API-Key security: - ApiKey: [] ``` ## API Testing and Mocking Comprehensive testing ensures reliable APIs: ```perl #!/usr/bin/env perl use Modern::Perl '2018'; use Test::More; use Test::Mojo; use Test::Deep; use Mojolicious::Lite; # Define test API get '/users' => sub { shift->render(json => [ { id => 1, name => 'Alice' }, { id => 2, name => 'Bob' }, ]); }; post '/users' => sub { my $c = shift; my $user = $c->req->json; $user->{id} = 3; $c->render(json => $user, status => 201); }; get '/users/:id' => sub { my $c = shift; my $id = $c->param('id'); if ($id == 1) { $c->render(json => { id => 1, name => 'Alice' }); } else { $c->render(json => { error => 'Not found' }, status => 404); } }; # Test the API my $t = Test::Mojo->new; # Test GET /users $t->get_ok('/users') ->status_is(200) ->header_is('Content-Type' => 'application/json;charset=UTF-8') ->json_is('/0/id' => 1) ->json_is('/0/name' => 'Alice') ->json_has('/1') ->json_hasnt('/2'); # Test POST /users $t->post_ok('/users' => json => { name => 'Charlie' }) ->status_is(201) ->json_is('/id' => 3) ->json_is('/name' => 'Charlie'); # Test GET /users/:id $t->get_ok('/users/1') ->status_is(200) ->json_is('/name' => 'Alice'); $t->get_ok('/users/999') ->status_is(404) ->json_is('/error' => 'Not found'); # Test with custom headers $t->get_ok('/users' => { 'X-Custom-Header' => 'test' }) ->status_is(200); # Deep comparison $t->get_ok('/users') ->json_is(bag( { id => 1, name => 'Alice' }, { id => 2, name => 'Bob' }, )); done_testing(); ``` ## Rate Limiting and Throttling Protect your APIs from abuse: ```perl #!/usr/bin/env perl use Mojolicious::Lite -signatures; use Cache::FastMmap; use Time::HiRes qw(time); # Initialize rate limiter cache my $cache = Cache::FastMmap->new( share_file => '/tmp/rate_limit.cache', cache_size => '1m', expire_time => 3600, ); # Rate limiting middleware helper rate_limit => sub ($c, $key, $max_requests, $window) { my $now = time(); my $window_start = int($now / $window) * $window; my $cache_key = "$key:$window_start"; # Get current count my $count = $cache->get($cache_key) || 0; if ($count >= $max_requests) { my $retry_after = $window_start + $window - $now; $c->res->headers->header('X-RateLimit-Limit' => $max_requests); $c->res->headers->header('X-RateLimit-Remaining' => 0); $c->res->headers->header('X-RateLimit-Reset' => $window_start + $window); $c->res->headers->header('Retry-After' => int($retry_after)); $c->render( json => { error => 'Rate limit exceeded' }, status => 429 ); return 0; } # Increment counter $cache->set($cache_key => ++$count); # Set headers $c->res->headers->header('X-RateLimit-Limit' => $max_requests); $c->res->headers->header('X-RateLimit-Remaining' => $max_requests - $count); $c->res->headers->header('X-RateLimit-Reset' => $window_start + $window); return 1; }; # Apply rate limiting to routes under sub ($c) { my $ip = $c->tx->remote_address; return $c->rate_limit("ip:$ip", 100, 60); # 100 requests per minute }; get '/api/data' => sub ($c) { # Additional stricter limit for this endpoint my $ip = $c->tx->remote_address; return unless $c->rate_limit("ip:$ip:data", 10, 60); # 10 requests per minute $c->render(json => { data => 'sensitive information' }); }; app->start; ``` ## Best Practices 1. **API Design** - Use consistent naming conventions - Version your APIs (/api/v1, /api/v2) - Implement proper HTTP status codes - Support pagination for large datasets 2. **Security** - Always use HTTPS in production - Implement authentication and authorization - Validate all input data - Rate limit to prevent abuse 3. **Performance** - Cache responses when appropriate - Use connection pooling - Implement request/response compression - Monitor API metrics 4. **Error Handling** - Return consistent error formats - Include helpful error messages - Log errors for debugging - Implement circuit breakers for external APIs 5. **Documentation** - Generate OpenAPI/Swagger specs - Provide code examples - Document rate limits and quotas - Keep changelog of API changes ## Summary Perl's web service capabilities have evolved tremendously. With modern frameworks like Mojolicious and comprehensive HTTP libraries, Perl excels at both consuming and providing APIs. Whether you're building microservices, integrating with third-party APIs, or creating real-time applications, Perl provides the tools and flexibility you need. The combination of CPAN's vast ecosystem, Perl's text processing prowess, and modern async capabilities makes it an excellent choice for API development in 2025. As distributed systems become ever more prevalent, Perl continues to prove its worth as a reliable, performant, and productive language for web services.