From 58904980f01cd7fdfc6f23188b66d0a8c8123462 Mon Sep 17 00:00:00 2001 From: Luka Leskovsek Date: Mon, 26 Nov 2018 11:22:40 +0100 Subject: [PATCH 1/6] Report query object. Add input argument. --- src/Command/ReportYearlyCommand.php | 29 +++++++++++++++---- src/Reports/ReportyViewCountByProfile.php | 35 +++++++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 src/Reports/ReportyViewCountByProfile.php diff --git a/src/Command/ReportYearlyCommand.php b/src/Command/ReportYearlyCommand.php index 97f026f..d7d598c 100755 --- a/src/Command/ReportYearlyCommand.php +++ b/src/Command/ReportYearlyCommand.php @@ -2,11 +2,13 @@ namespace BOF\Command; use Doctrine\DBAL\Driver\Connection; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use BOF\Reports\ReportyViewCountByProfile; + class ReportYearlyCommand extends ContainerAwareCommand { protected function configure() @@ -14,19 +16,36 @@ protected function configure() $this ->setName('report:profiles:yearly') ->setDescription('Page views report') - ; + ->addArgument('year', InputArgument::REQUIRED, 'Report year'); + } protected function execute(InputInterface $input, OutputInterface $output) { /** @var $db Connection */ - $io = new SymfonyStyle($input,$output); + $io = new SymfonyStyle($input, $output); $db = $this->getContainer()->get('database_connection'); - $profiles = $db->query('SELECT profile_name FROM profiles')->fetchAll(); + $year_argument = $input->getArgument('year'); + + if(strlen($year_argument) < 4){ + $output->write('Invalid input argument.' . PHP_EOL); + $output->write('N/A' . PHP_EOL); + exit; + } + + $output->write('Generating report for year : ' . $year_argument . PHP_EOL); + + $report = new ReportyViewCountByProfile(); + $profiles = $db->query($report->getByYear($year_argument))->fetchAll(); // Show data in a table - headers, data - $io->table(['Profile'], $profiles); + $io->table(['Profile ' . $year_argument, + 'Sum', 'Jan', 'Feb', + 'Mar', 'Apr', 'May', + 'Jun', 'Jul', 'Avg', + 'Sep', 'Oct', 'Nov', 'Dec'], + $profiles); } } diff --git a/src/Reports/ReportyViewCountByProfile.php b/src/Reports/ReportyViewCountByProfile.php new file mode 100644 index 0000000..9dbef82 --- /dev/null +++ b/src/Reports/ReportyViewCountByProfile.php @@ -0,0 +1,35 @@ + Date: Mon, 26 Nov 2018 12:14:48 +0100 Subject: [PATCH 2/6] Solu --- SOLUTION.md | 37 +++++++++++-- src/Command/ReportYearlyCommand.php | 63 +++++++++++++---------- src/Reports/ReportyViewCountByProfile.php | 35 ++++++------- 3 files changed, 87 insertions(+), 48 deletions(-) diff --git a/SOLUTION.md b/SOLUTION.md index defe675..2427803 100755 --- a/SOLUTION.md +++ b/SOLUTION.md @@ -3,11 +3,42 @@ SOLUTION Estimation ---------- -Estimated: n hours +Estimated: 5 hours -Spent: x hours +Spent: 3.5 hours Solution -------- -Comments on your solution +I'm not really familiar with Symfony framework so I'm making a lot of assumptions regarding the implementation process. I'm sure there are a lot out-of-the-box functionalities inside the Symfony framework and I'm also sure that this simple task could be done in x-y ways. +But I'm a quick learner and I always try to follow good coding practices (the general and best practices by each framework). + +In my opinion, a more suitable solution would be to build a RESTFull API for fetching report data. The service would accept input arguments and returned a structured JSON response object. But given that this is not a production code and you don't need to spend time on the presentational part of the app (view) it is a good fit. + +If I'm not wrong - the idea here is that you have a single command report class that is responsible for all Yearly reports (Views, Orders, Sales, ...). It would be more suitable that there would be a command-object-per-report (eg. for yearly views per profile). Each command class could use different query objects througout it's use - byYear , byYearAndProfile, ... . This way the code is much more structured in future proofed. + + +Test cases : +1. Running bin/console report:profiles:yearly '2016' + Feature : Get a yearly report on views per profile + Scanario : user runs the command and specifies input arguments (year) + Result : The system returns the date for a given year + Output : + +2. Running bin/console report:profiles:yearly '2016' + Feature : Get a yearly report on views per profile + Scanario : user runs the command and does not specifies input arguments (year) + Result : The system returns a message to the user explaining that the function has a mandatory argument year + Output : Not enough arguments (missing: "year"). + +3. Running bin/console report:profiles:yearly '2016' + Feature : Get a yearly report on views per profile + Scanario : user runs the command and an QUERY EXCEPTION ocurres stoping the execution. + Result : The system returns a message to the user explaining that there is a problem with the query passed to the report + Output : EXCEPTION : Exception ocurred while executing the query : + +4. Runnin bin/console report:profiles:yearly '2016' + Feature : Get a yearly report on views per profile + Scanario : user runs the command and an unknown program error ocurres stoping the execution. + Result : The system returns a message to the user explaining that there is a problem with the query passed to the report + Output : Something unexpected happened!! OH-My-OH-My. : You have requested a non-existent service "database_connectionww". diff --git a/src/Command/ReportYearlyCommand.php b/src/Command/ReportYearlyCommand.php index d7d598c..0392f34 100755 --- a/src/Command/ReportYearlyCommand.php +++ b/src/Command/ReportYearlyCommand.php @@ -1,14 +1,14 @@ getContainer()->get('database_connection'); - - $year_argument = $input->getArgument('year'); - - if(strlen($year_argument) < 4){ - $output->write('Invalid input argument.' . PHP_EOL); - $output->write('N/A' . PHP_EOL); - exit; - } - - $output->write('Generating report for year : ' . $year_argument . PHP_EOL); - - $report = new ReportyViewCountByProfile(); - - $profiles = $db->query($report->getByYear($year_argument))->fetchAll(); - // Show data in a table - headers, data - $io->table(['Profile ' . $year_argument, - 'Sum', 'Jan', 'Feb', - 'Mar', 'Apr', 'May', - 'Jun', 'Jul', 'Avg', - 'Sep', 'Oct', 'Nov', 'Dec'], - $profiles); - + try { + /** @var $db Connection */ + $io = new SymfonyStyle($input, $output); + + $db = $this->getContainer()->get('database_connection'); + $year_argument = $input->getArgument('year'); + + $output->write('Generating report for year : ' . $year_argument . PHP_EOL); + + $report = new ReportyViewCountByProfile(); + + try { + $profiles = $db->query($report->getByYear($year_argument))->fetchAll(); + } catch (\Doctrine\DBAL\Exception\SyntaxErrorException $ex) { + $output->write('EXCEPTION : Exception ocurred while executing the query : ' . PHP_EOL . 'QUERY : ' . $report->getByYear($year_argument)); + return 0; + }; + + //We fill a dummy row that represents no data row + if (sizeof($profiles) === 0) { + $profiles = array_fill(0, 1, array_fill(0, 13, 'N/A')); + } + + // Show data in a table - headers, data + $io->table(['Profile ' . $year_argument, + 'Sum', 'Jan', 'Feb', + 'Mar', 'Apr', 'May', + 'Jun', 'Jul', 'Avg', + 'Sep', 'Oct', 'Nov', 'Dec'], + $profiles); + + } catch (\Exception $ex) { + $output->write("Something unexpected happened!! OH-My-OH-My. : " . $ex->getMessage()); + }; } } diff --git a/src/Reports/ReportyViewCountByProfile.php b/src/Reports/ReportyViewCountByProfile.php index 9dbef82..430a519 100644 --- a/src/Reports/ReportyViewCountByProfile.php +++ b/src/Reports/ReportyViewCountByProfile.php @@ -6,29 +6,28 @@ class ReportyViewCountByProfile public function __construct() { - return $this; } public function getByYear($arg_year) { $profiles = ("SELECT pr.profile_name, count(vi.views) sum , - count(case when month(vi.date) = '1' then (vi.views) end) as Jan, - count(case when month(vi.date) = '2' then (vi.views) end) as 'Feb', - count(case when month(vi.date) = '3' then (vi.views) end) as 'Mar', - count(case when month(vi.date) = '4' then (vi.views) end) as 'Apr', - count(case when month(vi.date) = '5' then (vi.views) end) as 'May', - count(case when month(vi.date) = '6' then (vi.views) end) as 'Jun', - count(case when month(vi.date) = '7' then (vi.views) end) as 'Jul', - count(case when month(vi.date) = '8' then (vi.views) end) as 'Avg', - count(case when month(vi.date) = '9' then (vi.views) end) as 'Sep', - count(case when month(vi.date) = '10' then (vi.views) end) as 'Oct', - count(case when month(vi.date) = '11' then (vi.views) end) as 'Nov', - count(case when month(vi.date) = '12' then (vi.views) end) as 'Dec' - from profiles pr - left outer join views vi on pr.profile_id = vi.profile_id - where year(vi.date) = $arg_year - group by vi.profile_id, year(vi.date) - order by pr.profile_name"); + COUNT(CASE WHEN MONTH(vi.date) = '1' THEN (vi.views) END) as Jan, + COUNT(CASE WHEN MONTH(vi.date) = '2' THEN (vi.views) END) as 'Feb', + COUNT(CASE WHEN MONTH(vi.date) = '3' THEN (vi.views) END) as 'Mar', + COUNT(CASE WHEN MONTH(vi.date) = '4' THEN (vi.views) END) as 'Apr', + COUNT(CASE WHEN MONTH(vi.date) = '5' THEN (vi.views) END) as 'May', + COUNT(CASE WHEN MONTH(vi.date) = '6' THEN (vi.views) END) as 'Jun', + COUNT(CASE WHEN MONTH(vi.date) = '7' THEN (vi.views) END) as 'Jul', + COUNT(CASE WHEN MONTH(vi.date) = '8' THEN (vi.views) END) as 'Avg', + COUNT(CASE WHEN MONTH(vi.date) = '9' THEN (vi.views) END) as 'Sep', + COUNT(CASE WHEN MONTH(vi.date) = '10' THEN (vi.views) END) as 'Oct', + COUNT(CASE WHEN MONTH(vi.date) = '11' THEN (vi.views) END) as 'Nov', + COUNT(CASE WHEN MONTH(vi.date) = '12' THEN (vi.views) END) as 'Dec' + FROM profiles pr + LEFT JOIN views vi on pr.profile_id = vi.profile_id + WHERE year(vi.date) = $arg_year + GROUP BY vi.profile_id, year(vi.date) + ORDER BY pr.profile_name"); return $profiles; } From 2015fbb25cdaf0844c2629e2398d367b2b42ce31 Mon Sep 17 00:00:00 2001 From: Luka Leskovsek Date: Mon, 26 Nov 2018 12:30:57 +0100 Subject: [PATCH 3/6] Extra explanations. Wrong SELECT statement ... damn damn damn --- SOLUTION.md | 2 +- src/Reports/ReportyViewCountByProfile.php | 26 +++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/SOLUTION.md b/SOLUTION.md index 2427803..c024016 100755 --- a/SOLUTION.md +++ b/SOLUTION.md @@ -15,7 +15,7 @@ But I'm a quick learner and I always try to follow good coding practices (the ge In my opinion, a more suitable solution would be to build a RESTFull API for fetching report data. The service would accept input arguments and returned a structured JSON response object. But given that this is not a production code and you don't need to spend time on the presentational part of the app (view) it is a good fit. -If I'm not wrong - the idea here is that you have a single command report class that is responsible for all Yearly reports (Views, Orders, Sales, ...). It would be more suitable that there would be a command-object-per-report (eg. for yearly views per profile). Each command class could use different query objects througout it's use - byYear , byYearAndProfile, ... . This way the code is much more structured in future proofed. +If I'm not wrong - the idea here is that you have a single command report class that is responsible for all Yearly reports (Views, Orders, Sales, ...). It would be more suitable that there would be a command-object-per-report (eg. for yearly views per profile). Each command class could use different query objects througout it's use - byYear , byYearAndProfile, ... . This way the code is much more structured in future proofed. At this stage there is also no user authentication so each individual that has access to execute this command can view the results. Test cases : diff --git a/src/Reports/ReportyViewCountByProfile.php b/src/Reports/ReportyViewCountByProfile.php index 430a519..562714f 100644 --- a/src/Reports/ReportyViewCountByProfile.php +++ b/src/Reports/ReportyViewCountByProfile.php @@ -10,19 +10,19 @@ public function __construct() public function getByYear($arg_year) { - $profiles = ("SELECT pr.profile_name, count(vi.views) sum , - COUNT(CASE WHEN MONTH(vi.date) = '1' THEN (vi.views) END) as Jan, - COUNT(CASE WHEN MONTH(vi.date) = '2' THEN (vi.views) END) as 'Feb', - COUNT(CASE WHEN MONTH(vi.date) = '3' THEN (vi.views) END) as 'Mar', - COUNT(CASE WHEN MONTH(vi.date) = '4' THEN (vi.views) END) as 'Apr', - COUNT(CASE WHEN MONTH(vi.date) = '5' THEN (vi.views) END) as 'May', - COUNT(CASE WHEN MONTH(vi.date) = '6' THEN (vi.views) END) as 'Jun', - COUNT(CASE WHEN MONTH(vi.date) = '7' THEN (vi.views) END) as 'Jul', - COUNT(CASE WHEN MONTH(vi.date) = '8' THEN (vi.views) END) as 'Avg', - COUNT(CASE WHEN MONTH(vi.date) = '9' THEN (vi.views) END) as 'Sep', - COUNT(CASE WHEN MONTH(vi.date) = '10' THEN (vi.views) END) as 'Oct', - COUNT(CASE WHEN MONTH(vi.date) = '11' THEN (vi.views) END) as 'Nov', - COUNT(CASE WHEN MONTH(vi.date) = '12' THEN (vi.views) END) as 'Dec' + $profiles = ("SELECT pr.profile_name, FORMAT(SUM(vi.views),0) sum , + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '1' THEN (vi.views) END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '2' THEN (vi.views) END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '3' THEN (vi.views) END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '4' THEN (vi.views) END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '5' THEN (vi.views) END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '6' THEN (vi.views) END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '7' THEN (vi.views) END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '8' THEN (vi.views) END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '9' THEN (vi.views) END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '10' THEN (vi.views) END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '11' THEN (vi.views) END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '12' THEN (vi.views) END), 0) FROM profiles pr LEFT JOIN views vi on pr.profile_id = vi.profile_id WHERE year(vi.date) = $arg_year From 5b78a45601c67b6d31ea5cac15e052eed36abe75 Mon Sep 17 00:00:00 2001 From: Luka Leskovsek Date: Mon, 26 Nov 2018 12:34:34 +0100 Subject: [PATCH 4/6] Minor fixes. --- src/Command/ReportYearlyCommand.php | 2 +- src/Reports/ReportyViewCountByProfile.php | 24 +++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Command/ReportYearlyCommand.php b/src/Command/ReportYearlyCommand.php index 0392f34..98ef5bb 100755 --- a/src/Command/ReportYearlyCommand.php +++ b/src/Command/ReportYearlyCommand.php @@ -46,7 +46,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } // Show data in a table - headers, data - $io->table(['Profile ' . $year_argument, + $io->table(['Profile ' . str_replace("'", "", $year_argument), 'Sum', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Avg', diff --git a/src/Reports/ReportyViewCountByProfile.php b/src/Reports/ReportyViewCountByProfile.php index 562714f..31b9d7d 100644 --- a/src/Reports/ReportyViewCountByProfile.php +++ b/src/Reports/ReportyViewCountByProfile.php @@ -11,18 +11,18 @@ public function __construct() public function getByYear($arg_year) { $profiles = ("SELECT pr.profile_name, FORMAT(SUM(vi.views),0) sum , - FORMAT(SUM(CASE WHEN MONTH(vi.date) = '1' THEN (vi.views) END), 0), - FORMAT(SUM(CASE WHEN MONTH(vi.date) = '2' THEN (vi.views) END), 0), - FORMAT(SUM(CASE WHEN MONTH(vi.date) = '3' THEN (vi.views) END), 0), - FORMAT(SUM(CASE WHEN MONTH(vi.date) = '4' THEN (vi.views) END), 0), - FORMAT(SUM(CASE WHEN MONTH(vi.date) = '5' THEN (vi.views) END), 0), - FORMAT(SUM(CASE WHEN MONTH(vi.date) = '6' THEN (vi.views) END), 0), - FORMAT(SUM(CASE WHEN MONTH(vi.date) = '7' THEN (vi.views) END), 0), - FORMAT(SUM(CASE WHEN MONTH(vi.date) = '8' THEN (vi.views) END), 0), - FORMAT(SUM(CASE WHEN MONTH(vi.date) = '9' THEN (vi.views) END), 0), - FORMAT(SUM(CASE WHEN MONTH(vi.date) = '10' THEN (vi.views) END), 0), - FORMAT(SUM(CASE WHEN MONTH(vi.date) = '11' THEN (vi.views) END), 0), - FORMAT(SUM(CASE WHEN MONTH(vi.date) = '12' THEN (vi.views) END), 0) + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '1' THEN (vi.views) ELSE 0 END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '2' THEN (vi.views) ELSE 0 END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '3' THEN (vi.views) ELSE 0 END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '4' THEN (vi.views) ELSE 0 END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '5' THEN (vi.views) ELSE 0 END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '6' THEN (vi.views) ELSE 0 END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '7' THEN (vi.views) ELSE 0 END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '8' THEN (vi.views) ELSE 0 END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '9' THEN (vi.views) ELSE 0 END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '10' THEN (vi.views) ELSE 0 END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '11' THEN (vi.views) ELSE 0 END), 0), + FORMAT(SUM(CASE WHEN MONTH(vi.date) = '12' THEN (vi.views) ELSE 0 END), 0) FROM profiles pr LEFT JOIN views vi on pr.profile_id = vi.profile_id WHERE year(vi.date) = $arg_year From 6eca1844dce1b035575c5ff229088b0c66739684 Mon Sep 17 00:00:00 2001 From: Luka Leskovsek Date: Mon, 26 Nov 2018 12:51:56 +0100 Subject: [PATCH 5/6] More comments. --- SOLUTION.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/SOLUTION.md b/SOLUTION.md index c024016..fab9654 100755 --- a/SOLUTION.md +++ b/SOLUTION.md @@ -12,10 +12,11 @@ Solution -------- I'm not really familiar with Symfony framework so I'm making a lot of assumptions regarding the implementation process. I'm sure there are a lot out-of-the-box functionalities inside the Symfony framework and I'm also sure that this simple task could be done in x-y ways. But I'm a quick learner and I always try to follow good coding practices (the general and best practices by each framework). +After reading the Symfony Docs I'm prety sure that I should create a Service class for my ReportyViewCountByProfile class and not just a vanilla PHP class as I did. The idea is that my service class would be responsible for everything ; database ; queries ; input arguments, DI, etc. Current solution is not optimal and two classes are mixing responsibilities - which is not OK. The front-end class (ReportYearlyCommand) should only delegate execution to a internal Service class (ReportyViewCountByProfile) and this class should prepare the environment (db, query, ...) and then just return the results to the calling class which is still going to display the results. +Of course each report has to have it's own class with this strategy we avoid sphageti code and if we seperate our code there is a lot more +opportunity for code reuse. Me not using ORM was a decision by-design because I don't think that ORM is a good fit for reports&analytics - for performance reasons and query maintenance reasons the plan-sql is the right way to go. ORM is a good fit for basic CRUD actions. -In my opinion, a more suitable solution would be to build a RESTFull API for fetching report data. The service would accept input arguments and returned a structured JSON response object. But given that this is not a production code and you don't need to spend time on the presentational part of the app (view) it is a good fit. - -If I'm not wrong - the idea here is that you have a single command report class that is responsible for all Yearly reports (Views, Orders, Sales, ...). It would be more suitable that there would be a command-object-per-report (eg. for yearly views per profile). Each command class could use different query objects througout it's use - byYear , byYearAndProfile, ... . This way the code is much more structured in future proofed. At this stage there is also no user authentication so each individual that has access to execute this command can view the results. +In my opinion, a more suitable (production ready) solution would be to build a RESTFull API for fetching report data. The service would accept input arguments and returned a structured JSON response object. But given that this is not a production code and you don't need to spend time on the presentational part of the app (view) it is a good fit. With API calls we can more easly incoporate user authentication strategies, auditing, scaling, ... . Test cases : @@ -37,7 +38,7 @@ Test cases : Result : The system returns a message to the user explaining that there is a problem with the query passed to the report Output : EXCEPTION : Exception ocurred while executing the query : -4. Runnin bin/console report:profiles:yearly '2016' +4. Running bin/console report:profiles:yearly '2016' Feature : Get a yearly report on views per profile Scanario : user runs the command and an unknown program error ocurres stoping the execution. Result : The system returns a message to the user explaining that there is a problem with the query passed to the report From bb72aacdcf8e5eb3adf08b152fe14a7a06e84a1d Mon Sep 17 00:00:00 2001 From: Luka Leskovsek Date: Mon, 26 Nov 2018 12:52:51 +0100 Subject: [PATCH 6/6] Grammar fix. --- SOLUTION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SOLUTION.md b/SOLUTION.md index fab9654..44cb21f 100755 --- a/SOLUTION.md +++ b/SOLUTION.md @@ -16,7 +16,7 @@ After reading the Symfony Docs I'm prety sure that I should create a Service cla Of course each report has to have it's own class with this strategy we avoid sphageti code and if we seperate our code there is a lot more opportunity for code reuse. Me not using ORM was a decision by-design because I don't think that ORM is a good fit for reports&analytics - for performance reasons and query maintenance reasons the plan-sql is the right way to go. ORM is a good fit for basic CRUD actions. -In my opinion, a more suitable (production ready) solution would be to build a RESTFull API for fetching report data. The service would accept input arguments and returned a structured JSON response object. But given that this is not a production code and you don't need to spend time on the presentational part of the app (view) it is a good fit. With API calls we can more easly incoporate user authentication strategies, auditing, scaling, ... . +In my opinion, a more suitable (production ready) solution would be to build a RESTFull API for fetching report data. The service would accept input arguments and returned a structured JSON response object. But given that this is not a production code and you don't need to spend time on the presentational part of the app (view) it is a good fit. With API calls we can more easly incorporate user authentication strategies, auditing, scaling, ... . Test cases :