Add named parameter support for SQL queries

New Feature: Named SQL Parameters
- Supports both positional (?) and named (:name) parameters
- Named parameters are order-independent and more readable
- Syntax: sqlite_param :param_name $variable

Implementation:
- Updated sqlite_param directive to accept 1 or 2 arguments
- ModuleConfig.query_params now stores (name, variable) pairs
- execute_query() detects named vs positional parameters
- Extracted row_to_map closure to avoid type conflicts
- Named params use rusqlite named parameter binding

Examples (Port 8082):
- Book detail: WHERE id = :book_id
- Genre filter: WHERE genre = :genre_name
- Year range: WHERE year >= :min_year AND year <= :max_year
- Title search: WHERE title LIKE '%' || :search_term || '%'
- Rating filter: WHERE rating >= :min_rating

Benefits of Named Parameters:
- Order-independent: params can be in any order in config
- Self-documenting: :book_id is clearer than first ?
- Maintainable: can add/remove params without reordering
- Recommended for all but simplest queries

Configuration:
- conf/book_named_params.conf: Complete named params example
- start_named_params.sh: Quick start script for port 8082

Documentation:
- Added named vs positional comparison in README_PARAMETERS.md
- Updated README.md with named parameter examples
- Documented both syntaxes in directive reference

All examples tested and working with both parameter styles.
This commit is contained in:
Edward Langley
2025-11-15 15:20:40 -08:00
parent 775467da51
commit e016c2421b
7 changed files with 531 additions and 40 deletions

View File

@ -48,17 +48,29 @@ A full-featured catalog with category browsing, global templates, and responsive
**See:** `conf/book_catalog.conf` and `README_BOOK_CATALOG.md` **See:** `conf/book_catalog.conf` and `README_BOOK_CATALOG.md`
### Example 2: Parameterized Queries (Port 8081) ### Example 2: Positional Parameters (Port 8081)
Demonstrates dynamic SQL queries with nginx variables. Demonstrates dynamic SQL queries with positional parameters.
**Features:** **Features:**
- Book detail pages by ID - Query parameters with `?` placeholders
- Genre filtering with query parameters - Multiple positional parameters
- Year range searches with multiple parameters - Safe prepared statement binding
- Safe prepared statement parameter binding
**See:** `conf/book_detail.conf` and `README_PARAMETERS.md` **See:** `conf/book_detail.conf`
### Example 3: Named Parameters (Port 8082) - Recommended
Demonstrates named SQL parameters for better readability.
**Features:**
- Named parameters with `:name` syntax
- Order-independent parameter binding
- Title search with LIKE operator
- Rating filtering
- More maintainable configuration
**See:** `conf/book_named_params.conf` and `README_PARAMETERS.md`
## Configuration Directives ## Configuration Directives
@ -85,9 +97,14 @@ Specify the Handlebars template file (relative to location path).
### `sqlite_param` ### `sqlite_param`
Add a parameter to the SQL query (can be used multiple times). Add a parameter to the SQL query (can be used multiple times).
**Syntax:** `sqlite_param $variable_or_value;` **Syntax:**
- Positional: `sqlite_param $variable_or_value;`
- Named: `sqlite_param :param_name $variable_or_value;`
**Context:** `location` **Context:** `location`
**Notes:** Order matches `?` placeholders in query **Notes:**
- Positional parameters match `?` placeholders in order
- Named parameters match `:name` placeholders by name (recommended)
### `sqlite_global_templates` ### `sqlite_global_templates`
Set a directory for global template files (partials, layouts). Set a directory for global template files (partials, layouts).
@ -112,13 +129,22 @@ http {
sqlite_template "list.hbs"; sqlite_template "list.hbs";
} }
# Parameterized query # Parameterized query with named parameter (recommended)
location = /book { location = /book {
sqlite_db "catalog.db"; sqlite_db "catalog.db";
sqlite_query "SELECT * FROM books WHERE id = ?"; sqlite_query "SELECT * FROM books WHERE id = :book_id";
sqlite_param $arg_id; sqlite_param :book_id $arg_id;
sqlite_template "detail.hbs"; sqlite_template "detail.hbs";
} }
# Positional parameters also supported
location = /search {
sqlite_db "catalog.db";
sqlite_query "SELECT * FROM books WHERE year >= ? AND year <= ?";
sqlite_param $arg_min; # First ?
sqlite_param $arg_max; # Second ?
sqlite_template "list.hbs";
}
} }
} }
``` ```

View File

@ -8,13 +8,49 @@ The sqlite-serve module supports parameterized SQL queries using nginx variables
Add parameters to SQL queries. Can be used multiple times to add multiple parameters. Add parameters to SQL queries. Can be used multiple times to add multiple parameters.
**Syntax:** `sqlite_param variable_or_value;` **Syntax:**
- Positional: `sqlite_param variable_or_value;`
- Named: `sqlite_param :param_name variable_or_value;`
**Context:** `location` **Context:** `location`
**Multiple:** Yes (order matches `?` placeholders in query) **Multiple:** Yes
**Note:** Positional parameters match `?` placeholders in order. Named parameters match `:name` placeholders by name.
## Usage ## Usage
### Query Parameters (Most Common) ### Named Parameters (Recommended)
Named parameters provide better readability and don't depend on order:
```nginx
location = /book {
sqlite_db "book_catalog.db";
sqlite_query "SELECT * FROM books WHERE id = :book_id";
sqlite_param :book_id $arg_id; # Named parameter
sqlite_template "detail.hbs";
}
```
**Request:** `http://localhost/book?id=5`
**SQL Executed:** `SELECT * FROM books WHERE id = '5'`
### Multiple Named Parameters
```nginx
location = /years {
sqlite_db "book_catalog.db";
sqlite_query "SELECT * FROM books WHERE year >= :min AND year <= :max";
sqlite_param :min $arg_min; # Order doesn't matter
sqlite_param :max $arg_max; # with named params
sqlite_template "list.hbs";
}
```
**Request:** `http://localhost/years?min=2015&max=2024`
**SQL Executed:** `SELECT * FROM books WHERE year >= '2015' AND year <= '2024'`
### Query Parameters (Positional)
Use nginx's built-in `$arg_*` variables to access query parameters: Use nginx's built-in `$arg_*` variables to access query parameters:
@ -267,10 +303,46 @@ Run it with:
- All SQL placeholders must be `?` (positional parameters) - All SQL placeholders must be `?` (positional parameters)
- Parameters match placeholders in order of `sqlite_param` directives - Parameters match placeholders in order of `sqlite_param` directives
## Named vs Positional Parameters
### Named Parameters (`:name` syntax) - Recommended ✓
**Advantages:**
- Order-independent: Can rearrange `sqlite_param` directives without breaking queries
- Self-documenting: Parameter names explain their purpose
- Safer for maintenance: Adding/removing parameters less error-prone
- Better for complex queries with many parameters
**Example:**
```nginx
sqlite_query "SELECT * FROM books WHERE author = :author AND year > :year";
sqlite_param :year $arg_year; # Order doesn't matter!
sqlite_param :author $arg_author;
```
### Positional Parameters (`?` syntax)
**Advantages:**
- Slightly more compact configuration
- Works well for simple 1-2 parameter queries
**Disadvantages:**
- Order-dependent: Parameters must match `?` placeholders exactly
- Less readable with many parameters
- Error-prone when modifying queries
**Example:**
```nginx
sqlite_query "SELECT * FROM books WHERE author = ? AND year > ?";
sqlite_param $arg_author; # Must be first!
sqlite_param $arg_year; # Must be second!
```
**Recommendation:** Use named parameters (`:name`) for all but the simplest queries.
## Limitations ## Limitations
- Only supports `?` positional parameters (not named parameters like `:name`)
- Parameters must be provided in the exact order they appear in the query
- All parameter values are treated as strings (SQLite performs type coercion) - All parameter values are treated as strings (SQLite performs type coercion)
- Complex SQL values (arrays, JSON) should be constructed in the query itself - Complex SQL values (arrays, JSON) should be constructed in the query itself
- Cannot mix positional and named parameters in the same query

View File

@ -0,0 +1,74 @@
# Book Catalog with Named Parameters
# Demonstrates using named SQL parameters for better readability
load_module target/debug/libsqlite_serve.dylib;
worker_processes 1;
events {}
error_log logs/error.log debug;
http {
sqlite_global_templates "server_root/global_templates";
server {
listen 8082;
root "server_root";
# Book detail with named parameter
location = /book {
add_header "Content-Type" "text/html; charset=utf-8";
sqlite_db "book_catalog.db";
sqlite_query "SELECT * FROM books WHERE id = :book_id";
sqlite_param :book_id $arg_id;
sqlite_template "detail.hbs";
}
# Genre filter with named parameter
location = /genre {
add_header "Content-Type" "text/html; charset=utf-8";
sqlite_db "book_catalog.db";
sqlite_query "SELECT * FROM books WHERE genre = :genre_name ORDER BY rating DESC";
sqlite_param :genre_name $arg_genre;
sqlite_template "genre.hbs";
}
# Year range with named parameters
location = /years {
add_header "Content-Type" "text/html; charset=utf-8";
sqlite_db "book_catalog.db";
sqlite_query "SELECT * FROM books WHERE year >= :min_year AND year <= :max_year ORDER BY year DESC, title";
sqlite_param :min_year $arg_min;
sqlite_param :max_year $arg_max;
sqlite_template "list.hbs";
}
# Search by title with named parameter
location = /search {
add_header "Content-Type" "text/html; charset=utf-8";
sqlite_db "book_catalog.db";
sqlite_query "SELECT * FROM books WHERE title LIKE '%' || :search_term || '%' ORDER BY rating DESC, title";
sqlite_param :search_term $arg_q;
sqlite_template "list.hbs";
}
# Filter by rating with named parameter
location = /top-rated {
add_header "Content-Type" "text/html; charset=utf-8";
sqlite_db "book_catalog.db";
sqlite_query "SELECT * FROM books WHERE rating >= :min_rating ORDER BY rating DESC, title";
sqlite_param :min_rating $arg_rating;
sqlite_template "list.hbs";
}
# Fallback to all books
location / {
add_header "Content-Type" "text/html; charset=utf-8";
sqlite_db "book_catalog.db";
sqlite_query "SELECT * FROM books ORDER BY rating DESC, title";
sqlite_template "list.hbs";
}
}
}

122
server_root/search/list.hbs Normal file
View File

@ -0,0 +1,122 @@
{{> header}}
<style>
.book-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.book-card {
background: #fff;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 1.5rem;
transition: all 0.3s;
}
.book-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.2);
border-color: #667eea;
}
.book-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 0.5rem;
}
.book-title {
color: #2d3748;
font-size: 1.25rem;
margin-bottom: 0.25rem;
flex: 1;
}
.book-rating {
background: #fef3c7;
color: #92400e;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-weight: bold;
font-size: 0.9rem;
white-space: nowrap;
margin-left: 0.5rem;
}
.book-author {
color: #667eea;
font-weight: 500;
margin-bottom: 0.75rem;
}
.book-description {
color: #4a5568;
margin-bottom: 1rem;
line-height: 1.5;
}
.book-details {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
font-size: 0.875rem;
}
.book-genre {
background: #667eea;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-weight: 500;
}
.book-year {
background: #e9ecef;
color: #495057;
padding: 0.25rem 0.75rem;
border-radius: 4px;
}
.book-isbn {
color: #6c757d;
font-family: monospace;
font-size: 0.8rem;
}
.stats {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 2rem;
display: flex;
justify-content: space-around;
text-align: center;
}
.stat-item h2 {
font-size: 2.5rem;
margin-bottom: 0.25rem;
}
.stat-item p {
opacity: 0.9;
font-size: 0.9rem;
}
</style>
<div class="stats">
<div class="stat-item">
<h2>📚</h2>
<p>Book Collection</p>
</div>
<div class="stat-item">
<h2>⭐</h2>
<p>Top Rated</p>
</div>
<div class="stat-item">
<h2>🔍</h2>
<p>Browse All</p>
</div>
</div>
<h2 style="color: #2d3748; margin-bottom: 1rem;">All Books in Collection</h2>
<div class="book-grid">
{{#each results}}
{{> book_card}}
{{/each}}
</div>
{{> footer}}

View File

@ -0,0 +1,122 @@
{{> header}}
<style>
.book-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.book-card {
background: #fff;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 1.5rem;
transition: all 0.3s;
}
.book-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.2);
border-color: #667eea;
}
.book-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 0.5rem;
}
.book-title {
color: #2d3748;
font-size: 1.25rem;
margin-bottom: 0.25rem;
flex: 1;
}
.book-rating {
background: #fef3c7;
color: #92400e;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-weight: bold;
font-size: 0.9rem;
white-space: nowrap;
margin-left: 0.5rem;
}
.book-author {
color: #667eea;
font-weight: 500;
margin-bottom: 0.75rem;
}
.book-description {
color: #4a5568;
margin-bottom: 1rem;
line-height: 1.5;
}
.book-details {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
font-size: 0.875rem;
}
.book-genre {
background: #667eea;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-weight: 500;
}
.book-year {
background: #e9ecef;
color: #495057;
padding: 0.25rem 0.75rem;
border-radius: 4px;
}
.book-isbn {
color: #6c757d;
font-family: monospace;
font-size: 0.8rem;
}
.stats {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 2rem;
display: flex;
justify-content: space-around;
text-align: center;
}
.stat-item h2 {
font-size: 2.5rem;
margin-bottom: 0.25rem;
}
.stat-item p {
opacity: 0.9;
font-size: 0.9rem;
}
</style>
<div class="stats">
<div class="stat-item">
<h2>📚</h2>
<p>Book Collection</p>
</div>
<div class="stat-item">
<h2>⭐</h2>
<p>Top Rated</p>
</div>
<div class="stat-item">
<h2>🔍</h2>
<p>Browse All</p>
</div>
</div>
<h2 style="color: #2d3748; margin-bottom: 1rem;">All Books in Collection</h2>
<div class="book-grid">
{{#each results}}
{{> book_card}}
{{/each}}
</div>
{{> footer}}

View File

@ -1,7 +1,7 @@
use handlebars::Handlebars; use handlebars::Handlebars;
use ngx::core::Buffer; use ngx::core::Buffer;
use ngx::ffi::{ use ngx::ffi::{
ngx_hash_key, ngx_http_get_variable, NGX_CONF_TAKE1, NGX_HTTP_LOC_CONF, ngx_hash_key, ngx_http_get_variable, NGX_CONF_TAKE1, NGX_CONF_TAKE2, NGX_HTTP_LOC_CONF,
NGX_HTTP_MAIN_CONF, NGX_HTTP_MODULE, NGX_HTTP_LOC_CONF_OFFSET, NGX_RS_MODULE_SIGNATURE, NGX_HTTP_MAIN_CONF, NGX_HTTP_MODULE, NGX_HTTP_LOC_CONF_OFFSET, NGX_RS_MODULE_SIGNATURE,
nginx_version, ngx_chain_t, ngx_command_t, ngx_conf_t, ngx_http_module_t, ngx_int_t, nginx_version, ngx_chain_t, ngx_command_t, ngx_conf_t, ngx_http_module_t, ngx_int_t,
ngx_module_t, ngx_str_t, ngx_uint_t, ngx_module_t, ngx_str_t, ngx_uint_t,
@ -46,7 +46,7 @@ struct ModuleConfig {
db_path: String, db_path: String,
query: String, query: String,
template_path: String, template_path: String,
query_params: Vec<String>, // Variable names to use as query parameters query_params: Vec<(String, String)>, // (param_name, variable_name) pairs
} }
// Global configuration for shared templates // Global configuration for shared templates
@ -179,7 +179,7 @@ static mut ngx_http_howto_commands: [ngx_command_t; 6] = [
}, },
ngx_command_t { ngx_command_t {
name: ngx_string!("sqlite_param"), name: ngx_string!("sqlite_param"),
type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) as ngx_uint_t, type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1 | NGX_CONF_TAKE2) as ngx_uint_t,
set: Some(ngx_http_howto_commands_add_param), set: Some(ngx_http_howto_commands_add_param),
conf: NGX_HTTP_LOC_CONF_OFFSET, conf: NGX_HTTP_LOC_CONF_OFFSET,
offset: 0, offset: 0,
@ -272,8 +272,20 @@ extern "C" fn ngx_http_howto_commands_add_param(
unsafe { unsafe {
let conf = &mut *(conf as *mut ModuleConfig); let conf = &mut *(conf as *mut ModuleConfig);
let args = (*(*cf).args).elts as *mut ngx_str_t; let args = (*(*cf).args).elts as *mut ngx_str_t;
let param = (*args.add(1)).to_string(); let nelts = (*(*cf).args).nelts;
conf.query_params.push(param);
if nelts == 2 {
// Single argument: positional parameter
// sqlite_param $arg_id
let variable = (*args.add(1)).to_string();
conf.query_params.push((String::new(), variable));
} else if nelts == 3 {
// Two arguments: named parameter
// sqlite_param :book_id $arg_id
let param_name = (*args.add(1)).to_string();
let variable = (*args.add(2)).to_string();
conf.query_params.push((param_name, variable));
}
}; };
std::ptr::null_mut() std::ptr::null_mut()
@ -318,7 +330,7 @@ fn load_templates_from_dir(reg: &mut Handlebars, dir_path: &str) -> std::io::Res
fn execute_query( fn execute_query(
db_path: &str, db_path: &str,
query: &str, query: &str,
params: &[&str], params: &[(String, String)], // (param_name, value) pairs
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> { ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
let conn = Connection::open(db_path)?; let conn = Connection::open(db_path)?;
let mut stmt = conn.prepare(query)?; let mut stmt = conn.prepare(query)?;
@ -328,13 +340,11 @@ fn execute_query(
.map(|i| stmt.column_name(i).unwrap_or("").to_string()) .map(|i| stmt.column_name(i).unwrap_or("").to_string())
.collect(); .collect();
// Convert params to rusqlite parameters // Bind parameters (either positional or named)
let rusqlite_params: Vec<&dyn rusqlite::ToSql> = params let has_named_params = params.iter().any(|(name, _)| !name.is_empty());
.iter()
.map(|p| p as &dyn rusqlite::ToSql) // Convert row to JSON map
.collect(); let row_to_map = |row: &rusqlite::Row| -> rusqlite::Result<std::collections::HashMap<String, serde_json::Value>> {
let rows = stmt.query_map(rusqlite_params.as_slice(), |row| {
let mut map = std::collections::HashMap::new(); let mut map = std::collections::HashMap::new();
for (i, col_name) in column_names.iter().enumerate() { for (i, col_name) in column_names.iter().enumerate() {
let value: serde_json::Value = match row.get_ref(i)? { let value: serde_json::Value = match row.get_ref(i)? {
@ -350,7 +360,8 @@ fn execute_query(
} }
rusqlite::types::ValueRef::Blob(v) => { rusqlite::types::ValueRef::Blob(v) => {
// Convert blob to hex string // Convert blob to hex string
let hex_string = v.iter() let hex_string = v
.iter()
.map(|b| format!("{:02x}", b)) .map(|b| format!("{:02x}", b))
.collect::<String>(); .collect::<String>();
serde_json::Value::String(hex_string) serde_json::Value::String(hex_string)
@ -359,7 +370,23 @@ fn execute_query(
map.insert(col_name.clone(), value); map.insert(col_name.clone(), value);
} }
Ok(map) Ok(map)
})?; };
let rows = if has_named_params {
// Use named parameters
let named_params: Vec<(&str, &dyn rusqlite::ToSql)> = params
.iter()
.map(|(name, value)| (name.as_str(), value as &dyn rusqlite::ToSql))
.collect();
stmt.query_map(named_params.as_slice(), row_to_map)?
} else {
// Use positional parameters
let positional_params: Vec<&dyn rusqlite::ToSql> = params
.iter()
.map(|(_, value)| value as &dyn rusqlite::ToSql)
.collect();
stmt.query_map(positional_params.as_slice(), row_to_map)?
};
rows.collect() rows.collect()
} }
@ -407,8 +434,8 @@ http_request_handler!(howto_access_handler, |request: &mut http::Request| {
.unwrap_or(""); .unwrap_or("");
// Resolve query parameters from nginx variables // Resolve query parameters from nginx variables
let mut param_values: Vec<String> = Vec::new(); let mut param_values: Vec<(String, String)> = Vec::new();
for var_name in &co.query_params { for (param_name, var_name) in &co.query_params {
let value = if var_name.starts_with('$') { let value = if var_name.starts_with('$') {
// It's a variable reference, resolve it from nginx // It's a variable reference, resolve it from nginx
let var_name_str = &var_name[1..]; // Remove the '$' prefix let var_name_str = &var_name[1..]; // Remove the '$' prefix
@ -445,19 +472,17 @@ http_request_handler!(howto_access_handler, |request: &mut http::Request| {
// It's a literal value // It's a literal value
var_name.clone() var_name.clone()
}; };
param_values.push(value); param_values.push((param_name.clone(), value));
} }
ngx_log_debug_http!( ngx_log_debug_http!(
request, request,
"executing query with {} parameters: {:?}", "executing query with {} parameters",
param_values.len(), param_values.len()
param_values
); );
// Execute the configured SQL query with parameters // Execute the configured SQL query with parameters
let param_refs: Vec<&str> = param_values.iter().map(|s| s.as_str()).collect(); let results = match execute_query(&co.db_path, &co.query, &param_values) {
let results = match execute_query(&co.db_path, &co.query, &param_refs) {
Ok(results) => results, Ok(results) => results,
Err(e) => { Err(e) => {
ngx_log_debug_http!(request, "failed to execute query: {}", e); ngx_log_debug_http!(request, "failed to execute query: {}", e);

50
start_named_params.sh Executable file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Start script for named parameters example
set -e
echo "📚 Starting Named Parameters Example..."
echo ""
# Check if database exists
if [ ! -f "book_catalog.db" ]; then
echo "Database not found. Running setup..."
./setup_book_catalog.sh
echo ""
fi
# Check if module is built
if [ ! -f "target/debug/libsqlite_serve.dylib" ]; then
echo "Module not built. Building..."
direnv exec "$PWD" cargo build
echo ""
fi
# Start nginx
echo "Starting nginx on http://localhost:8082"
./ngx_src/nginx-1.28.0/objs/nginx -c conf/book_named_params.conf -p .
echo ""
echo "✅ Named Parameters Example is running!"
echo ""
echo "Named Parameter Examples:"
echo " • http://localhost:8082/book?id=1"
echo " Query: SELECT * FROM books WHERE id = :book_id"
echo " Param: :book_id = \$arg_id"
echo ""
echo " • http://localhost:8082/genre?genre=Programming"
echo " Query: ... WHERE genre = :genre_name"
echo " Param: :genre_name = \$arg_genre"
echo ""
echo " • http://localhost:8082/years?min=2015&max=2024"
echo " Query: ... WHERE year >= :min_year AND year <= :max_year"
echo " Params: :min_year = \$arg_min, :max_year = \$arg_max"
echo ""
echo " • http://localhost:8082/search?q=Rust"
echo " Search by title with named parameter"
echo ""
echo " • http://localhost:8082/top-rated?rating=4.7"
echo " Filter by minimum rating"
echo ""
echo "To stop: ./ngx_src/nginx-1.28.0/objs/nginx -s stop -c conf/book_named_params.conf -p ."