Тест на вакансию

Использование фреймворка Mojolicious для Perl при создании API

13 января 2026 г.
81

Установка Mojolicious

Убедимся, что Mojolicious установлен:
cpan Mojolicious

Или используем cpanm:
cpanm Mojolicious

Создадим новое Mojolicious-приложение:
mojo generate app APIApp
cd apiapp

Установим зависимости:
cpan DBI DBD::mysql Digest::SHA

Структура проекта

  • lib/
    • APIApp/
      • Model/
        • User.pm
        • Article.pm
      • Controller/
        • Users.pm
        • Articles.pm
    • APIApp.pm
  • public/
    • index.html
  • script/
    • apiapp
    • create_database.pl
    • drop_database.pl
  • t/
    • basic.t
  • a_p_i_app.conf

Файл конфигурации

{
    # Настройки Mojolicious
    hypnotoad => {
        listen  => ['http://*:3000'],
        workers => 4,
    },
   
    # Настройки базы данных
    database => {
        driver   => 'mysql',
        database => 'api_app_db',
        host     => 'localhost',
        port     => 3306,
        user     => 'root',
        password => '',
        options  => {
            mysql_enable_utf8 => 1,
            RaiseError        => 1,
            AutoCommit        => 1,
        }
    },
   
    # Настройки приложения
    app_name => 'APIApp',
    secret   => 'your-secret-key-change-this-in-production',
}

Запускаемый скрипт

#!/usr/bin/env perl

use strict;
use warnings;

use FindBin;
BEGIN { unshift @INC, "$FindBin::Bin/../lib" }

use APIApp;

# Запуск приложения
my $app = APIApp->new;
$app->start;

Основной файл приложения

lib/APIApp.pm:
package APIApp;
use Mojo::Base 'Mojolicious';

use DBI;

sub startup {
    my $self = shift;
   
    # Чтение конфигурации
    $self->plugin('Config');
   
    # Подключение к базе данным
    $self->helper(db => sub {
        my $c = shift;
        my $config = $c->app->config->{database};
       
        state $dbh = DBI->connect(
            "DBI:$config->{driver}:database=$config->{database};host=$config->{host};port=$config->{port}",
            $config->{user},
            $config->{password},
            $config->{options}
        ) or die "Could not connect to database: $DBI::errstr";
       
        return $dbh;
    });
   
    # Хелперы для моделей
    $self->helper(users => sub {
        my $c = shift;
        require APIApp::Model::User;
        state $model = APIApp::Model::User->new(dbh => $c->db);
        return $model;
    });
   
    $self->helper(articles => sub {
        my $c = shift;
        require APIApp::Model::Article;
        state $model = APIApp::Model::Article->new(dbh => $c->db);
        return $model;
    });
   
    # Настройка маршрутов
    my $r = $self->routes;
   
    # Health check
    $r->get('/')->to(cb => sub {
        my $c = shift;
        $c->render(json => {
            app     => 'APIApp',
            version => '1.0',
            status  => 'running',
            time    => scalar localtime
        });
    });
   
    # API маршруты
    my $api = $r->under('/api/v1');
   
    # Пользователи
    $api->get('/users')->to('users#list');
    $api->get('/users/:id')->to('users#show');
    $api->post('/users')->to('users#create');
    $api->put('/users/:id')->to('users#update');
    $api->delete('/users/:id')->to('users#delete');
   
    # Статьи
    $api->get('/articles')->to('articles#list');
    $api->get('/articles/:id')->to('articles#show');
    $api->post('/articles')->to('articles#create');
    $api->put('/articles/:id')->to('articles#update');
    $api->delete('/articles/:id')->to('articles#delete');
   
    # Статьи пользователя
    $api->get('/users/:user_id/articles')->to('articles#list_by_user');
   
    # Статьи по статусу
    $api->get('/articles/status/:status')->to('articles#list_by_status');
   
    # Хелпер для JSON ответов
    $self->helper(json_response => sub {
        my ($c, $success, $data, $error, $status) = @_;
        $status ||= $success ? 200 : 400;
       
        my $response = { success => $success };
        $response->{data} = $data if defined $data;
        $response->{error} = $error if defined $error;
       
        $c->render(json => $response, status => $status);
    });
}

1;

Создадим отдельные скрипты для миграций

Содание БД и таблиц (script/create_database.pl):
#!/usr/bin/env perl

use strict;
use warnings;
use utf8;
use open qw(:utf8 :std);
use FindBin;
use lib "$FindBin::Bin/../lib";

# Загружаем Mojolicious и Config
use Mojolicious::Commands;
use Mojolicious;

# Создаем приложение Mojolicious
my $app = Mojolicious->new;

# Устанавливаем путь к конфигурационному файлу
$app->config(hypnotoad => {});

# Загружаем конфигурацию
my $config_file = "$FindBin::Bin/../a_p_i_app.conf";
$app->plugin('Config' => {file => $config_file});

# Получаем конфигурацию БД
my $db_config = $app->config('database') || {};

# Настройки базы данных
my %db_config = (
    host     => $db_config->{host} || 'localhost',
    port     => $db_config->{port} || 3306,
    user     => $db_config->{user} || 'root',
    password => $db_config->{password} || '',
    database => $db_config->{database} || 'api_app_db'
);

# Обработка аргументов командной строки
my $seed_data = 0;
foreach my $arg (@ARGV) {
    if ($arg eq '--seed') {
        $seed_data = 1;
    }
    elsif ($arg eq '--help') {
        usage();
        exit 0;
    }
}

# Подключаем DBI
use DBI;

# Подключение к MySQL
my $dbh = DBI->connect(
    "DBI:mysql:host=$db_config{host};port=$db_config{port}",
    $db_config{user},
    $db_config{password},
    {
        RaiseError => 1,
        PrintError => 0,
        mysql_enable_utf8 => 1,
    }
) or die "Cannot connect to MySQL: $DBI::errstr";
print "=" x 60 . "\n";
print "API App Database Setup\n";
print "Config file: $config_file\n";
print "=" x 60 . "\n\n";
print "Creating database '$db_config{database}'...\n";

# Создание базы данных
$dbh->do("CREATE DATABASE IF NOT EXISTS $db_config{database} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
$dbh->do("USE $db_config{database}");

# Таблица пользователей
print "Creating 'users' table...\n";
$dbh->do("
    CREATE TABLE IF NOT EXISTS users (
        id INT AUTO_INCREMENT PRIMARY KEY,
        username VARCHAR(50) UNIQUE NOT NULL,
        email VARCHAR(100) UNIQUE NOT NULL,
        password VARCHAR(255) NOT NULL,
        full_name VARCHAR(100),
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        INDEX idx_username (username),
        INDEX idx_email (email)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");

# Таблица статей
print "Creating 'articles' table...\n";
$dbh->do("
    CREATE TABLE IF NOT EXISTS articles (
        id INT AUTO_INCREMENT PRIMARY KEY,
        user_id INT NOT NULL,
        title VARCHAR(255) NOT NULL,
        slug VARCHAR(255),
        content TEXT NOT NULL,
        excerpt VARCHAR(500),
        status ENUM('draft', 'published', 'archived') DEFAULT 'draft',
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        published_at TIMESTAMP NULL,
        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
        INDEX idx_user_id (user_id),
        INDEX idx_status (status),
        INDEX idx_slug (slug),
        INDEX idx_published_at (published_at)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");

# Создание индексов с проверкой существования
print "\nCreating indexes...\n";
create_index_if_not_exists($dbh, 'articles', 'idx_articles_title', 'title(100)');
create_index_if_not_exists($dbh, 'users', 'idx_users_created_at', 'created_at');

# Создание тестовых данных
if ($seed_data) {
    print "\n" . "=" x 60 . "\n";
    print "Seeding with test data...\n";
    print "=" x 60 . "\n";
   
    seed_test_data($dbh, $db_config{database});
}
print "\n" . "=" x 60 . "\n";
print "Database setup completed successfully!\n";
print "Database: $db_config{database}\n";
print "Host: $db_config{host}:$db_config{port}\n";
print "=" x 60 . "\n";
$dbh->disconnect;

# Функция для создания индекса только если он не существует
sub create_index_if_not_exists {
    my ($dbh, $table, $index_name, $columns) = @_;
   
    my $sth = $dbh->prepare("
        SELECT COUNT(1)
        FROM INFORMATION_SCHEMA.STATISTICS
        WHERE table_schema = DATABASE()
          AND table_name = ?
          AND index_name = ?
    ");
    $sth->execute($table, $index_name);
   
    if ($sth->fetchrow_array() == 0) {
        eval {
            $dbh->do("CREATE INDEX $index_name ON $table($columns)");
            print "  Created index $index_name on $table\n";
        };
        if ($@) {
            warn "  Failed to create index $index_name: $@\n";
        }
    } else {
        print "  Index $index_name already exists on $table\n";
    }
}

# Функция для заполнения тестовыми данными
sub seed_test_data {
    my ($dbh, $database) = @_;
   
    # Используем указанную базу данных
    $dbh->do("USE $database");
     # Устанавливаем кодировку соединения
    $dbh->do("SET NAMES utf8mb4");
   
    # Список тестовых пользователей
    my @test_users = (
        {
            username => 'admin',
            email    => 'admin@example.ru',
            password => 'admin123',
            full_name => 'Administrator'
        },
        {
            username => 'eugene_starodubtsev',
            email    => 'eugene@example.ru',
            password => 'password123',
            full_name => 'Eugene Starodubtsev'
        },
        {
            username => 'julia_ivanova',
            email    => 'julia@example.ru',
            password => 'password123',
            full_name => 'Juila Ivanova'
        }
    );
   
    # Вставляем пользователей
    foreach my $user (@test_users) {
        my $sth = $dbh->prepare("SELECT id FROM users WHERE username = ? OR email = ?");
        $sth->execute($user->{username}, $user->{email});
       
        if (my $existing_id = $sth->fetchrow_array()) {
            print "  User '$user->{username}' already exists (ID: $existing_id)\n";
            $user->{id} = $existing_id;
        } else {
            $dbh->do("
                INSERT INTO users (username, email, password, full_name)
                VALUES (?, ?, SHA2(?, 256), ?)
            ", undef,
                $user->{username},
                $user->{email},
                $user->{password},
                $user->{full_name});
           
            $user->{id} = $dbh->last_insert_id(undef, undef, undef, undef);
            print "  Created user '$user->{username}' (ID: $user->{id})\n";
        }
    }
   
    # Тестовые статьи для каждого пользователя
    my @articles = (
        {
            title => 'Начало работы с Perl',
            slug  => 'getting-started-with-perl',
            content => 'Perl — мощный язык программирования...',
            excerpt => 'Изучите основы программирования на Perl',
            status => 'published'
        },
        {
            title => 'Веб-фреймворк Mojolicious',
            slug  => 'mojolicious-web-framework',
            content => 'Mojolicious — веб-фреймворк реального времени...',
            excerpt => 'Введение в фреймворк Mojolicious',
            status => 'published'
        },
        {
            title => 'Лучшие практики MySQL',
            slug  => 'mysql-best-practices',
            content => 'MySQL — популярная реляционная база данных...',
            excerpt => 'Советы для оптимизация производительности MySQL',
            status => 'published'
        },
        {
            title => 'Принципы проектирования API',
            slug  => 'api-design-principles',
            content => 'Хороший дизайн API имеет решающее значение для успеха...',
            excerpt => 'Ключевые принципы проектирования RESTful API',
            status => 'draft'
        },
        {
            title => 'Docker для разработки',
            slug  => 'docker-for-development',
            content => 'Docker упрощает рабочие процессы разработки...',
            excerpt => 'Использование Docker в вашей среде разработки',
            status => 'published'
        }
    );
   
    # Вставляем статьи
    my $article_count = 0;
    foreach my $user (@test_users) {
        foreach my $article (@articles) {
            my $sth = $dbh->prepare("SELECT id FROM articles WHERE slug = ?");
            $sth->execute($article->{slug});
           
            if (!$sth->fetchrow_array()) {
                $dbh->do("
                    INSERT INTO articles (user_id, title, slug, content, excerpt, status, published_at)
                    VALUES (?, ?, ?, ?, ?, ?,
                        CASE WHEN ? = 'published' THEN NOW() ELSE NULL END)
                ", undef,
                    $user->{id},
                    $article->{title},
                    $article->{slug},
                    $article->{content},
                    $article->{excerpt},
                    $article->{status},
                    $article->{status});
               
                $article_count++;
            }
        }
    }
   
    print "\n  Created $article_count test articles\n";
   
    # Показать статистику
    print "\nDatabase Statistics:\n";
    print "  " . "-" x 40 . "\n";
   
    my $sth = $dbh->prepare("SELECT COUNT(*) FROM users");
    $sth->execute();
    print "  Users: " . $sth->fetchrow_array() . "\n";
   
    $sth = $dbh->prepare("SELECT COUNT(*) FROM articles");
    $sth->execute();
    print "  Articles: " . $sth->fetchrow_array() . "\n";
   
    $sth = $dbh->prepare("SELECT COUNT(*) FROM articles WHERE status = 'published'");
    $sth->execute();
    print "  Published articles: " . $sth->fetchrow_array() . "\n";
}

sub usage {
    print <<"END_USAGE";
Usage: $0 [options]
Options:
    --seed    Seed database with test data
    --help    Show this help message
Examples:
    $0                    # Create database structure only
    $0 --seed            # Create database with test data
Configuration:
    The script reads configuration from a_p_i_app.conf
    Expected structure:
    {
        "database": {
            "host": "localhost",
            "port": 3306,
            "user": "root",
            "password": "your_password",
            "database": "api_app_db"
        }
    }
END_USAGE
    exit;
}
 
Удаление БД и таблиц (script/drop_database.pl):
#!/usr/bin/env perl

use strict;
use warnings;
use DBI;
use FindBin;
use lib "$FindBin::Bin/../lib";

# Используем Mojolicious для чтения конфига
use Mojolicious;
my $app = Mojolicious->new;

# Устанавливаем путь к конфигурационному файлу
$app->config(hypnotoad => {});

# Загружаем конфигурацию
my $config_file = "$FindBin::Bin/../a_p_i_app.conf";
$app->plugin('Config' => {file => $config_file});

# Получаем конфигурацию БД
my $db_config = $app->config('database') || {};

# Настройки базы данных
my $host = $db_config->{host} || 'localhost';
my $port = $db_config->{port} || 3306;
my $user = $db_config->{user} || 'root';
my $password = $db_config->{password} || '';
my $database = $db_config->{database} || 'api_app_db';
print "=" x 60 . "\n";
print "Database Drop Script\n";
print "Config file: a_p_i_app.conf\n";
print "=" x 60 . "\n\n";
print "Database to drop: $database\n";
print "Host: $host:$port\n";
print "User: $user\n\n";

# Запрос подтверждения
print "WARNING: This will permanently delete database '$database'!\n";
print "Are you sure you want to continue? (yes/no): ";
my $response = <STDIN>;
chomp $response;
unless ($response =~ /^yes$/i) {
    print "Operation cancelled.\n";
    exit 0;
}

# Подключение
my $dbh = eval {
    DBI->connect(
        "DBI:mysql:host=$host;port=$port",
        $user,
        $password,
        { RaiseError => 1, PrintError => 0 }
    );
};
if (!$dbh) {
    die "Cannot connect to MySQL: $DBI::errstr\n";
}

# Дополнительная проверка - список баз данных
my $sth = $dbh->prepare("SHOW DATABASES LIKE ?");
$sth->execute($database);
if (!$sth->fetchrow_array()) {
    print "Database '$database' does not exist.\n";
    $dbh->disconnect;
    exit 0;
}

# Удаление базы данных
print "\nDropping database '$database'...\n";
eval {
    $dbh->do("DROP DATABASE IF EXISTS $database");
};
if ($@) {
    print "Error dropping database: $@\n";
    $dbh->disconnect;
    exit 1;
}
print " Database '$database' dropped successfully!\n";

# Проверяем, что база действительно удалена
$sth = $dbh->prepare("SHOW DATABASES LIKE ?");
$sth->execute($database);
if (!$sth->fetchrow_array()) {
    print " Confirmed: database '$database' no longer exists.\n";
} else {
    print " Warning: database '$database' might still exist.\n";
}
$dbh->disconnect;
print "\n" . "=" x 60 . "\n";
print "Operation completed.\n";
print "=" x 60 . "\n";

Создание БД и таблиц:
perl script/create_database.pl

Создание БД и таблиц с тестовыми данными:
perl script/create_database.pl --seed

Удаление БД:
perl script/drop_database.pl

Модели

Модель пользователей (lib/APIApp/Model/User.pm):
package APIApp::Model::User;
use Mojo::Base -base;

has 'dbh';

# Найти по ID
sub find {
    my ($self, $id) = @_;
    my $sth = $self->dbh->prepare("SELECT * FROM users WHERE id = ?");
    $sth->execute($id);
    return $sth->fetchrow_hashref;
}

# Найти по имени пользователя
sub find_by_username {
    my ($self, $username) = @_;
    my $sth = $self->dbh->prepare("SELECT * FROM users WHERE username = ?");
    $sth->execute($username);
    return $sth->fetchrow_hashref;
}

# Найти по email
sub find_by_email {
    my ($self, $email) = @_;
    my $sth = $self->dbh->prepare("SELECT * FROM users WHERE email = ?");
    $sth->execute($email);
    return $sth->fetchrow_hashref;
}

# Создать пользователя
sub create {
    my ($self, $data) = @_;
   
    # Хеширование пароля
    require Digest::SHA;
    $data->{password} = Digest::SHA::sha256_hex($data->{password});
   
    my $sql = "INSERT INTO users (username, email, password, full_name) VALUES (?, ?, ?, ?)";
    my $sth = $self->dbh->prepare($sql);
    $sth->execute($data->{username}, $data->{email}, $data->{password}, $data->{full_name});
   
    return $self->find($self->dbh->last_insert_id(undef, undef, undef, undef));
}

# Обновить пользователя
sub update {
    my ($self, $id, $data) = @_;
   
    my @fields;
    my @values;
   
    # Хеширование пароля если он предоставлен
    if ($data->{password}) {
        require Digest::SHA;
        $data->{password} = Digest::SHA::sha256_hex($data->{password});
    }
   
    for my $field (qw(username email password full_name)) {
        if (exists $data->{$field}) {
            push @fields, "$field = ?";
            push @values, $data->{$field};
        }
    }
   
    return unless @fields;
   
    push @values, $id;
    my $sql = "UPDATE users SET " . join(', ', @fields) . " WHERE id = ?";
    $self->dbh->prepare($sql)->execute(@values);
   
    return $self->find($id);
}

# Удалить пользователя
sub delete {
    my ($self, $id) = @_;
    my $sth = $self->dbh->prepare("DELETE FROM users WHERE id = ?");
    return $sth->execute($id);
}

# Все пользователи
sub all {
    my $self = shift;
    my $sth = $self->dbh->prepare("
        SELECT id, username, email, full_name, created_at
        FROM users
        ORDER BY id
    ");
    $sth->execute();
    return $sth->fetchall_arrayref({});
}

1;
Модель статей (lib/APIApp/Model/Article.pm):
package APIApp::Model::Article;
use Mojo::Base -base;

has 'dbh';

# Найти по ID
sub find {
    my ($self, $id) = @_;
    my $sth = $self->dbh->prepare("
        SELECT a.*, u.username, u.email
        FROM articles a
        LEFT JOIN users u ON a.user_id = u.id
        WHERE a.id = ?
    ");
    $sth->execute($id);
    return $sth->fetchrow_hashref;
}

# Создать статью
sub create {
    my ($self, $data) = @_;
   
    my $sql = "INSERT INTO articles (user_id, title, content, status) VALUES (?, ?, ?, ?)";
    my $sth = $self->dbh->prepare($sql);
    $sth->execute(
        $data->{user_id},
        $data->{title},
        $data->{content},
        $data->{status} || 'draft'
    );
   
    return $self->find($self->dbh->last_insert_id(undef, undef, undef, undef));
}

# Обновить статью
sub update {
    my ($self, $id, $data) = @_;
   
    my @fields;
    my @values;
   
    for my $field (qw(title content status)) {
        if (exists $data->{$field}) {
            push @fields, "$field = ?";
            push @values, $data->{$field};
        }
    }
   
    return unless @fields;
   
    push @values, $id;
    my $sql = "UPDATE articles SET " . join(', ', @fields) . " WHERE id = ?";
    $self->dbh->prepare($sql)->execute(@values);
   
    return $self->find($id);
}

# Удалить статью
sub delete {
    my ($self, $id) = @_;
    my $sth = $self->dbh->prepare("DELETE FROM articles WHERE id = ?");
    return $sth->execute($id);
}

# Все статьи
sub all {
    my $self = shift;
    my $sth = $self->dbh->prepare("
        SELECT a.*, u.username
        FROM articles a
        LEFT JOIN users u ON a.user_id = u.id
        ORDER BY a.created_at DESC
    ");
    $sth->execute();
    return $sth->fetchall_arrayref({});
}

# Статьи пользователя
sub by_user {
    my ($self, $user_id) = @_;
    my $sth = $self->dbh->prepare("
        SELECT a.*, u.username
        FROM articles a
        LEFT JOIN users u ON a.user_id = u.id
        WHERE a.user_id = ?
        ORDER BY a.created_at DESC
    ");
    $sth->execute($user_id);
    return $sth->fetchall_arrayref({});
}

# Статьи с фильтрацией по статусу
sub by_status {
    my ($self, $status) = @_;
    my $sth = $self->dbh->prepare("
        SELECT a.*, u.username
        FROM articles a
        LEFT JOIN users u ON a.user_id = u.id
        WHERE a.status = ?
        ORDER BY a.created_at DESC
    ");
    $sth->execute($status);
    return $sth->fetchall_arrayref({});
}

# Поиск с пагинацией
sub search {
    my ($self, $page, $per_page) = @_;
    $page ||= 1;
    $per_page ||= 20;
    my $offset = ($page - 1) * $per_page;
   
    my $sth = $self->dbh->prepare("
        SELECT a.*, u.username
        FROM articles a
        LEFT JOIN users u ON a.user_id = u.id
        ORDER BY a.created_at DESC
        LIMIT ? OFFSET ?
    ");
    $sth->execute($per_page, $offset);
    return $sth->fetchall_arrayref({});
}

# Подсчет статей пользователя
sub count_by_user {
    my ($self, $user_id) = @_;
    my $sth = $self->dbh->prepare("
        SELECT COUNT(*) as count
        FROM articles
        WHERE user_id = ?
    ");
    $sth->execute($user_id);
    my $result = $sth->fetchrow_hashref;
    return $result->{count};
}

1;

Контроллеры

Контроллер пользователей (lib/APIApp/Controller/Users.pm):
package APIApp::Controller::Users;
use Mojo::Base 'Mojolicious::Controller';

sub list {
    my $c = shift;
   
    my $users = $c->users->all();
   
    $c->json_response(1, $users);
}

sub show {
    my $c = shift;
    my $id = $c->param('id');
   
    my $user = $c->users->find($id);
   
    if ($user) {
        delete $user->{password}; # Не показываем пароль
        $c->json_response(1, $user);
    } else {
        $c->json_response(0, undef, 'User not found', 404);
    }
}

sub create {
    my $c = shift;
    my $data = $c->req->json;
   
    # Валидация
    unless ($data && $data->{username} && $data->{email} && $data->{password}) {
        return $c->json_response(0, undef, 'Missing required fields: username, email, password');
    }
   
    # Проверка уникальности имени пользователя
    if ($c->users->find_by_username($data->{username})) {
        return $c->json_response(0, undef, 'Username already exists', 409);
    }
   
    # Проверка уникальности email
    if ($c->users->find_by_email($data->{email})) {
        return $c->json_response(0, undef, 'Email already exists', 409);
    }
   
    # Создаем пользователя
    my $user = $c->users->create($data);
   
    if ($user) {
        delete $user->{password};
        $c->json_response(1, $user, undef, 201);
    } else {
        $c->json_response(0, undef, 'Failed to create user', 500);
    }
}

sub update {
    my $c = shift;
    my $id = $c->param('id');
    my $data = $c->req->json;
   
    unless ($data && %$data) {
        return $c->json_response(0, undef, 'No data provided for update');
    }
   
    # Проверяем существование пользователя
    my $existing_user = $c->users->find($id);
    unless ($existing_user) {
        return $c->json_response(0, undef, 'User not found', 404);
    }
   
    # Проверка уникальности username, если он меняется
    if ($data->{username} && $data->{username} ne $existing_user->{username}) {
        if ($c->users->find_by_username($data->{username})) {
            return $c->json_response(0, undef, 'Username already exists', 409);
        }
    }
   
    # Проверка уникальности email, если он меняется
    if ($data->{email} && $data->{email} ne $existing_user->{email}) {
        if ($c->users->find_by_email($data->{email})) {
            return $c->json_response(0, undef, 'Email already exists', 409);
        }
    }
   
    my $user = $c->users->update($id, $data);
   
    if ($user) {
        delete $user->{password};
        $c->json_response(1, $user);
    } else {
        $c->json_response(0, undef, 'Failed to update user', 500);
    }
}

sub delete {
    my $c = shift;
    my $id = $c->param('id');
   
    # Проверяем существование пользователя
    unless ($c->users->find($id)) {
        return $c->json_response(0, undef, 'User not found', 404);
    }
   
    if ($c->users->delete($id)) {
        $c->json_response(1, {message => 'User deleted successfully'});
    } else {
        $c->json_response(0, undef, 'Failed to delete user', 500);
    }
}

1;
Контроллер статей (lib/APIApp/Controller/Articles.pm):
package APIApp::Controller::Articles;
use Mojo::Base 'Mojolicious::Controller';

sub list {
    my $c = shift;
    my $articles = $c->articles->all();
    $c->json_response(1, $articles);
}

sub show {
    my $c = shift;
    my $id = $c->param('id');
    my $article = $c->articles->find($id);
   
    if ($article) {
        $c->json_response(1, $article);
    } else {
        $c->json_response(0, undef, 'Article not found', 404);
    }
}

sub create {
    my $c = shift;
    my $data = $c->req->json;
   
    unless ($data && $data->{user_id} && $data->{title} && $data->{content}) {
        return $c->json_response(0, undef, 'Missing required fields: user_id, title, content');
    }
   
    # Проверяем существование пользователя
    unless ($c->users->find($data->{user_id})) {
        return $c->json_response(0, undef, 'User not found', 404);
    }
   
    my $article = $c->articles->create($data);
   
    if ($article) {
        $c->json_response(1, $article, undef, 201);
    } else {
        $c->json_response(0, undef, 'Failed to create article', 500);
    }
}

sub update {
    my $c = shift;
    my $id = $c->param('id');
    my $data = $c->req->json;
   
    unless ($data && %$data) {
        return $c->json_response(0, undef, 'No data provided');
    }
   
    # Проверяем существование статьи
    unless ($c->articles->find($id)) {
        return $c->json_response(0, undef, 'Article not found', 404);
    }
   
    my $article = $c->articles->update($id, $data);
   
    if ($article) {
        $c->json_response(1, $article);
    } else {
        $c->json_response(0, undef, 'Failed to update article', 500);
    }
}

sub delete {
    my $c = shift;
    my $id = $c->param('id');
   
    # Проверяем существование статьи
    unless ($c->articles->find($id)) {
        return $c->json_response(0, undef, 'Article not found', 404);
    }
   
    if ($c->articles->delete($id)) {
        $c->json_response(1, {message => 'Article deleted successfully'});
    } else {
        $c->json_response(0, undef, 'Failed to delete article', 500);
    }
}

sub list_by_user {
    my $c = shift;
    my $user_id = $c->param('user_id');
   
    # Проверяем существование пользователя
    unless ($c->users->find($user_id)) {
        return $c->json_response(0, undef, 'User not found', 404);
    }
   
    my $articles = $c->articles->by_user($user_id);
    $c->json_response(1, $articles);
}

sub list_by_status {
    my $c = shift;
    my $status = $c->param('status');
   
    # Валидация статуса
    my @valid_statuses = qw(draft published archived);
    unless (grep { $_ eq $status } @valid_statuses) {
        return $c->json_response(0, undef, 'Invalid status. Valid values: draft, published, archived', 400);
    }
   
    my $articles = $c->articles->by_status($status);
    $c->json_response(1, $articles);
}

1;

Тестирование API

Запустим сервер Morbo (для тестирования):
morbo script/apiapp
Сервер запустится и будет доступен по адресу: http://localhost:3000

Запуск через обычного демона:
perl script/apiapp daemon -l http://*:3000
Или с HTTPS:
perl script/apiapp daemon -l https://*:443 --cert /path/to/cert

Запуск Hypnotoad (продакшн-сервер Mojolicious):
hypnotoad script/apiapp

Проверка работоспособности:
curl http://localhost:3000/

Создание пользователя:
curl -X POST http://localhost:3000/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{"username":"testuser","email":"test@example.com","password":"test123","full_name":"Test User"}'
Получение списка пользователей:
curl http://localhost:3000/api/v1/users
Создание статьи:
curl -X POST http://localhost:3000/api/v1/articles \
  -H "Content-Type: application/json" \
  -d '{"user_id":1,"title":"Первая статья про Perl","content":"Это моя самая первая статья про Perl","status":"published"}'
Получение статей пользователя:
curl http://localhost:3000/api/v1/users/1/articles
Удаление статьи:
curl -X DELETE http://localhost:3000/api/v1/articles/1 \
  -H "Content-Type: application/json"
Обновление пользователя:
curl -X PUT http://localhost:3000/api/v1/users/1 \
  -H "Content-Type: application/json" \
  -d '{"full_name":"Updated Name","email":"updated@example.com"}'
Mojolicious представляет собой современный и мощный фреймворк для Perl, идеально подходящий для создания RESTful API благодаря своей асинхронной архитектуре и минималистичному подходу. С его помощью можно быстро разрабатывать производительные веб-приложения с поддержкой WebSockets, шаблонизацией и встроенным тестированием. Фреймворк особенно ценен для микросервисов и API благодаря легковесности и отсутствию внешних зависимостей.
Поделиться: