clear light bulb planter on gray rock

Twitter recently recommended a tweet to me (all hail the algorithm) touting what the author viewed as the top 5 web development stacks.”

JavaScript/​Node.js options dominated the four-​letter acronyms as expected, but the fifth one surprised me: LAMP, the combination of the Linux operating system, Apache web server, MySQL relational database, and Perl, PHP, or Python programming languages. A quick web search for similar lists yielded similar results. Clearly, this meme (in the Dawkins sense) has outlasted its popularization by tech publisher O’Reilly in the 2000s.

Originally coined in 1998 during the dot-​com” bubble, I had thought that the term LAMP” had faded with developers in the intervening decades with the rise of language-​specific web frameworks for:

Certainly on the Perl side (with which I’m most familiar), the community has long since recommended the use of a framework built on the PSGI specification, deprecating 1990s-​era CGI scripts and the mod_​perl Apache extension. Although general-​purpose web servers like Apache or Nginx may be part of an overall system, they’re typically used as proxies or load balancers for Perl-​specific servers either provided by the framework or a third-​party module.

Granted, PHP still relies on web server-​specific modules, APIs, or variations of the FastCGI protocol for interfacing with a web server. And Python web applications typically make use of its WSGI protocol either as a web server extension or, like the Perl examples above, as a proxied standalone server. But all of these are deployment details and do little to describe how developers implement and extend a web application’s structure.

Note how the various four-​letter JavaScript stacks (e.g., MERN, MEVN, MEAN, PERN) differentiate themselves mostly by frontend framework (e.g., Angular, React, Vue.js) and maybe by the (relational or NoSQL) database (e.g., MongoDB, MySQL, PostgreSQL). All however seem standardized on the Node.js runtime and Express backend web framework, which could, in theory, be replaced with non-​JavaScript options like the more mature LAMP-​associated languages and frameworks. (Or if you prefer languages that don’t start with P”, there’s C#, Go, Java, Ruby, etc.)

My point is that LAMP” as the name of a web development stack has outlived its usefulness. It’s at once too specific (about operating system and web server details that are often abstracted away for developers) and too broad (covering three separate programming languages and not the frameworks they favor). It also leaves out other non-​JavaScript back-​end languages and their associated frameworks.

The question is: what can replace it? I’d propose NoJS” as reminiscent of NoSQL,” but that inaccurately excludes JavaScript from its necessary role in the front-​end. NJSB” doesn’t exactly roll off the tongue, either, and still has the same ambiguity problem as LAMP.”

How about pithy sort-​of-​acronyms patterned like database-​frontend-​backend? Here are some Perl examples:

  • MRDancer: MySQL, React, and Dancer (I use this at work. Yes, the M could also stand for MongoDB. Naming things is hard.)
  • MRMojo: MongoDB, React, and Mojolicious
  • PACat: PostgreSQL, Angular, and Catalyst
  • etc.

Ultimately it comes down to community and industry adoption. If you’re involved with back-​end web development, please let me know in the comments if you agree or disagree that LAMP” is still a useful term, and if not, what should replace it.

In March I wrote The Perl debugger can be your superpower, introducing the step debugger as a better way to debug your Perl code rather than littering your source with temporary print statements or logging. I use the debugger all the time, and I’ve realized that some more techniques are worth covering.

Although I mentioned a caveat when debugging web applications, our apps at work all adhere to the Perl Web Server Gateway Interface (PSGI) specification and thus we can use tools like Test::WWW::Mechanize::PSGI or Plack::Test to run tests and debugging sessions in the same Perl process. (Mojolicious users can use something like Test::Mojo for the same effect.)

To demonstrate, let’s get started with something like this which tests that a given route (/say-hello) returns a certain JSON structure ({"message": "Hello world!"}):

#!/usr/bin/env perl

use Test::Most;
use Test::WWW::Mechanize::PSGI;
use JSON::MaybeXS;
use Local::MyApp; # name of app's main module

my $mech = Test::WWW::Mechanize::PSGI->new(
    # a Dancer2 app, so to_app returns a PSGI coderef
    app => Local::MyApp->to_app(),
);
$mech->get_ok('/say-hello');
lives_and {
    my $json = decode_json($mech->content);
    cmp_deeply( $json, {message => 'Hello world!'} );
} 'message is Hello world!';

done_testing;

All very fine and well, but what happens if that route starts returning a different message or worse, invalid output that causes decode_json to fail? Eventually, you’ll rewrite the test in the script to output the offending content when something goes wrong, but right now you want to suss out the root cause.

Debuggers have the concept of breakpoints, which are flags that tell the debugger to stop at a certain line of code and wait for instructions. We can set them while running the debugger with the b command or continue to a one-​time breakpoint with the c command, or we can insert them into the code ourselves before running it through the debugger in the first place.

Add this line right after the lives_and { line:

$DB::single = 1;

This simulates having typed the s command in the debugger at that line, stopping execution at that point. Run our test with perl’s -d option, and then type c to continue to that breakpoint:

$ perl -d -Ilib t/test_psgi.t

Loading DB routines from perl5db.pl version 1.60
Editor support available.

Enter h or 'h h' for help, or 'man perldebug' for more help.

[Local::MyApp:7170] core @2021-07-06 07:33:22> Built config from files: /Users/mgardner/Projects/blog/myapp/config.yml /Users/mgardner/Projects/blog/myapp/environments/development.yml in (eval 310)[/Users/mgardner/.plenv/versions/5.34.0/lib/perl5/site_perl/5.34.0/Sub/Quote.pm:3] l. 910
Test2::API::CODE(0x7ffabea39ee8)(/Users/mgardner/.plenv/versions/5.34.0/lib/perl5/site_perl/5.34.0/Test2/API.pm:71):
71:	    INIT { eval 'END { test2_set_is_end() }; 1' or die $@ }

  DB<1> c

[...]
ok 1 - GET /say-hello
main::CODE(0x7f8069caf2c8)(t/test_psgi.t:14):
15:	    my $json = decode_json($mech->content);

  DB<1> 

From here we can examine variables, set other breakpoints, or even execute arbitrary lines of code. Let’s see what became of that HTTP GET request:

  DB<1> x $mech->content

0  '{"error":"Undefined subroutine &Local::MyApp::build_frog called at lib/Local/MyApp.pm line 11.\\n"}'

  DB<2> 

Aha, something has returned some different JSON indicating an error. Let’s look at the lines around (1020) the offending line (11):

  DB<2> f lib/Local/MyApp.pm

  DB<3> l 10-20

10:	        my $method = 'build_frog';
11:	        $method->();
12 	    }
13:	    catch ($e) {
14:	        send_as JSON => {error => $e};
15 	    }
16:	    send_as JSON => {message => 'Hello world!'};
17:	};
18
19 	sub build_frob {
20:	    return;

  DB<4>

Yep, a typo on line 11, and one that wasn’t caught at compile time since it’s generated at runtime.

Just to be sure (and to demonstrate some other cool debugger features), let’s set another breakpoint while in the debugger and then exercise that route again. Then we’ll check that $method variable against the list of available methods in the Local::MyApp package.

  DB<4> b 11

  DB<5> $mech->get('/say-hello')

[...]
Local::MyApp::CODE(0x7f8066f2db60)(lib/Local/MyApp.pm:11):
11:	        $method->();

  DB<<6>> x $method

0  'build_frog'

  DB<<7>> m Local::MyApp
any
app
body_parameters
build_frob
captures
config
content
[...]
  DB<<8>>

No doubt about it, that variable is being set incorrectly.

Quit out of the debugger with the q command, make the fix (we probably want errors to give something other than an HTTP 200 OK while we’re at it), and re-​run the test:

$ perl -Ilib t/test_psgi.t

[Local::MyApp:8277] core @2021-07-06 07:48:36> Built config from files: /Users/mgardner/Projects/blog/myapp/config.yml /Users/mgardner/Projects/blog/myapp/environments/development.yml in (eval 309) l. 910
Name "DB::single" used only once: possible typo at t/test_psgi.t line 13.
[...]
ok 1 - GET /say-hello
ok 2 - message is Hello world!
1..2

Note that warning about leaving $DB::single in there. While harmless, it’s a good reminder to remove such lines from your code so that they don’t surprise you or your teammates during future debugging sessions.

And that’s it. Note that because we’re using PSGI, we were able to set breakpoints in our web app code itself and the debugger stopped there and enabled us to have a look around. And as you’ve seen, once you’re at a breakpoint you can switch to different files, add/​remove more breakpoints, run arbitrary code, and more. The perldebug documentation page has all the details.

Happy debugging! For your reference, here’s the full app module and test script used in this article:

MyApp.pm

package Local::MyApp;
use Dancer2;
use Feature::Compat::Try;

our $VERSION = '0.1';

get '/say-hello' => sub {
    try {
        no strict 'refs';
        my $method = 'build_frob';
        $method->();
    }
    catch ($e) {
        status 'error';
        send_as JSON => {error => $e};
    }
    send_as JSON => {message => 'Hello world!'};
};

sub build_frob {
    return;
}

true;

test_psgi.t

#!/usr/bin/env perl

use Test::Most;
use Test::WWW::Mechanize::PSGI;
use JSON::MaybeXS;
use Local::MyApp; # name of your app's main module goes here

my $mech = Test::WWW::Mechanize::PSGI->new(
    # a Dancer2 app, so to_app returns a PSGI coderef
    app => Local::MyApp->to_app(),
);
$mech->get_ok('/say-hello');
lives_and {
    my $json = decode_json($mech->content);
    cmp_deeply( $json, {message => 'Hello world!'} );
} 'message is Hello world!';

done_testing;