Lightning-fast HTTP development server in pure Perl. No dependencies, no bloat, just serve.
Sometimes you just need to spin up a quick server. No frameworks, no complexity, no waiting for npm install. Just pure Perl doing what it does best: getting out of your way and getting stuff done.
Built for developers who value simplicity over ceremony.
# Basic usage - serve current directory on port 8080
perl tinyserve.pl
# Custom port and directory
perl tinyserve.pl --port 3000 --root ./dist
# Verbose mode to see everything
perl tinyserve.pl --verboseThat's it. Your server is running.
Static File Serving • Automatic MIME type detection for 20+ formats
Custom Routes • Define API endpoints with GET, POST, PUT, DELETE
JSON Native • Built-in JSON parsing and response handling
Concurrent Connections • Handle 50+ simultaneous connections
Request Logging • Detailed timing and request information
Hot Reloadable • Edit routes without restart
Zero Config • Sensible defaults, works out of the box
Middleware Support • Chain request/response processors
No installation needed. Just grab the script:
curl -O https://raw.githubusercontent.com/yourrepo/tinyserve/main/tinyserve.pl
chmod +x tinyserve.pl
./tinyserve.plRequirements: Perl 5.10+ (already on most systems)
Optional modules for full features:
cpanm JSON::PP URI::Escape Time::HiResperl tinyserve.pl [OPTIONS]| Option | Description | Default |
|---|---|---|
--port <PORT> |
Port to listen on | 8080 |
--host <HOST> |
Host to bind to | 0.0.0.0 |
--root <PATH> |
Document root directory | ./public |
--verbose |
Enable verbose logging | Off |
--max-connections <N> |
Max concurrent connections | 50 |
--timeout <SEC> |
Connection timeout | 30 |
--help |
Show help message | - |
TinyServe comes with example endpoints to get you started:
Server health check and info.
curl http://localhost:8080/api/status{
"status": "ok",
"version": "1.0.0",
"uptime": 1234567890
}Echo back request data - perfect for testing.
curl -X POST http://localhost:8080/api/echo \
-H "Content-Type: application/json" \
-d '{"test": "data"}'{
"method": "POST",
"path": "/api/echo",
"headers": {...},
"body": "{\"test\": \"data\"}",
"json": {"test": "data"},
"params": {}
}Add your own API endpoints by editing the script:
# Simple JSON API
register_route('GET', '/api/users', sub {
my ($req, $res) = @_;
$res->{status} = 200;
$res->{headers}{'Content-Type'} = 'application/json';
$res->{body} = encode_json({
users => [
{ id => 1, name => 'Alice' },
{ id => 2, name => 'Bob' }
]
});
});
# Handle POST with JSON body
register_route('POST', '/api/users', sub {
my ($req, $res) = @_;
my $data = $req->{json}; # Automatically parsed
$res->{status} = 201;
$res->{headers}{'Content-Type'} = 'application/json';
$res->{body} = encode_json({
id => 3,
name => $data->{name}
});
});
# Access query parameters
register_route('GET', '/api/search', sub {
my ($req, $res) = @_;
my $query = $req->{params}{q}; # From ?q=search
$res->{status} = 200;
$res->{headers}{'Content-Type'} = 'application/json';
$res->{body} = encode_json({
query => $query,
results => []
});
});Add cross-cutting concerns with middleware:
# CORS middleware
add_middleware(sub {
my ($req, $res) = @_;
$res->{headers}{'Access-Control-Allow-Origin'} = '*';
$res->{headers}{'Access-Control-Allow-Methods'} = 'GET, POST, PUT, DELETE';
return 1; # Continue processing
});
# Authentication middleware
add_middleware(sub {
my ($req, $res) = @_;
if ($req->{path} =~ m{^/api/admin}) {
my $auth = $req->{headers}{authorization} || '';
unless ($auth eq 'Bearer secret-token') {
$res->{status} = 401;
$res->{headers}{'Content-Type'} = 'application/json';
$res->{body} = encode_json({ error => 'Unauthorized' });
return 0; # Stop processing
}
}
return 1; # Continue
});
# Logging middleware
add_middleware(sub {
my ($req, $res) = @_;
log_message("DEBUG", "Processing: $req->{method} $req->{path}");
return 1;
});Every route handler receives a request object:
{
method => 'POST', # HTTP method
path => '/api/users', # URL path
uri => '/api/users?page=1', # Full URI
query_string => 'page=1', # Raw query string
headers => { # Request headers (lowercase keys)
'content-type' => 'application/json',
'user-agent' => 'curl/7.68.0'
},
body => '{"name":"Alice"}', # Raw request body
json => { name => 'Alice' }, # Parsed JSON (if Content-Type: application/json)
params => { page => 1 } # Query params + form data
}Modify the response object to send data:
{
status => 200, # HTTP status code
headers => { # Response headers
'Content-Type' => 'application/json',
'X-Custom-Header' => 'value'
},
body => '{"result":"success"}' # Response body
}TinyServe automatically detects and serves these file types:
Web: html, htm, css, js, json, xml, txt
Images: png, jpg, jpeg, gif, svg, ico
Fonts: woff, woff2, ttf
Documents: pdf, zip
Video: mp4, webm
Unknown types default to application/octet-stream.
# Serve your React/Vue/Angular build
perl tinyserve.pl --root ./dist --port 3000# Quick mock API for testing
register_route('GET', '/api/products', sub {
my ($req, $res) = @_;
$res->{headers}{'Content-Type'} = 'application/json';
$res->{body} = encode_json([
{ id => 1, name => 'Widget', price => 19.99 },
{ id => 2, name => 'Gadget', price => 29.99 }
]);
});register_route('POST', '/upload', sub {
my ($req, $res) = @_;
# Save uploaded file
open my $fh, '>', 'uploads/file.dat';
print $fh $req->{body};
close $fh;
$res->{status} = 201;
$res->{headers}{'Content-Type'} = 'application/json';
$res->{body} = encode_json({ success => 1 });
});# Use TinyServe for static files, proxy WS elsewhere
perl tinyserve.pl --port 8080 &
websocat -s 8081 ws://production-server.com/ws &# Serve files with verbose logging
perl tinyserve.pl --verbose --root ./src
# In another terminal, watch for changes
watch -n 1 'echo "Files changed at $(date)"'# Handle more simultaneous connections
perl tinyserve.pl --max-connections 100# Longer timeout for slow clients
perl tinyserve.pl --timeout 60
# Shorter timeout for fast networks
perl tinyserve.pl --timeout 5# Bind to localhost for security
perl tinyserve.pl --host 127.0.0.1TinyServe is designed for development only. Do not use in production.
- No HTTPS support
- Basic directory traversal protection
- No rate limiting
- No authentication by default
- Verbose error messages
For production, use Apache, Nginx, or a proper application server.
[2025-01-15 10:23:45] [INFO] TinyServe v1.0.0 started
[2025-01-15 10:23:45] [INFO] Listening on http://0.0.0.0:8080
[2025-01-15 10:23:50] GET /index.html - 200 - 5.23ms
[2025-01-15 10:23:51] GET /api/status - 200 - 1.15ms
perl tinyserve.pl --verboseShows full request headers and body (first 200 chars).
# Check what's using the port
lsof -i :8080
# Or use a different port
perl tinyserve.pl --port 8081# Ports below 1024 need root
sudo perl tinyserve.pl --port 80
# Or use a higher port
perl tinyserve.pl --port 8080# Install missing Perl modules
cpanm JSON::PP URI::Escape Time::HiRes IO::Socket::INET# Check your document root
perl tinyserve.pl --root ./public --verbose
# List files being served
ls -la ./public| Feature | TinyServe | Python SimpleHTTPServer | Node http-server |
|---|---|---|---|
| Startup Time | Instant | ~1s | ~2s |
| Memory Usage | <10MB | ~30MB | ~50MB |
| Custom Routes | Yes | No | Limited |
| JSON Support | Built-in | No | No |
| Dependencies | Perl stdlib | Python | Node + npm |
| Hot Reload | Edit & run | Restart | Restart |
[Unit]
Description=TinyServe Development Server
After=network.target
[Service]
Type=simple
User=youruser
WorkingDirectory=/path/to/project
ExecStart=/usr/bin/perl /path/to/tinyserve.pl --port 8080 --root ./public
Restart=always
[Install]
WantedBy=multi-user.target# Add to ~/.bashrc or ~/.zshrc
alias serve='perl ~/bin/tinyserve.pl'
# Now just type:
serve
serve --port 3000# Format API responses with jq
curl http://localhost:8080/api/status | jq .# Apache Bench
ab -n 1000 -c 10 http://localhost:8080/
# wrk
wrk -t4 -c100 -d30s http://localhost:8080/TinyServe is intentionally minimal. PRs welcome for:
- Bug fixes
- Performance improvements
- Additional MIME types
- Better error handling
Please keep it simple and dependency-free.
MIT License - use it however you want.
Built with Perl and a passion for simplicity.
Philosophy: The best server is the one that gets out of your way.
Made by developer, for developers • Star it if you find it useful!