Flask search endpoint
Create a search endpoint in Flask — Python web development tutorial.
Focus: create a search endpoint in flask
You've built Flask routes that return data. But when your users want to find something among those records — a product by name, a post by keyword, a user by email — the panic sets in. How do you wire up a /search endpoint without slowing everything to a crawl or accidentally exposing your entire database? In this lesson, you'll learn to create a search endpoint in Flask that filters data predictably, handles edge cases, and stays fast enough for real-world use. We'll move from a naive route to a production-minded pattern, with mistakes to avoid and alternatives to consider.
The problem this lesson solves
A search bar in your web app seems simple. But as soon as you write SELECT * FROM products WHERE name = ?, you realize it's not. The browser sends a query string like /search?q=laptop, and you need to:
- Extract the query parameter safely (it might be missing or empty).
- Filter your data — from a list, a database, or an API — without returning everything.
- Handle weird input:
%, spaces, Unicode, case sensitivity. - Keep response times predictable as your data grows.
Without a deliberate design, you'll end up with ugly code like if 'q' in request.args scattered everywhere, and you'll hit 500 errors or slow responses the first time a user types a phrase with a space. This lesson gives you a clear, repeatable pattern to create a search endpoint in Flask that works in development and scales in production.
Core concept / mental model
Think of a search endpoint as a filter pipeline. The client sends a request with a search term. Your job is to:
- Receive the term (or terms) from the query string.
- Sanitize it — strip whitespace, decide on case sensitivity, maybe split into words.
- Filter your data source using the sanitized term.
- Return a JSON response with results (and maybe a count).
Imagine a physical library catalog. You walk up to the librarian and say, "Books about snakes." The librarian doesn't dump the entire library on your lap; they scan the title/index for "snakes," maybe check the author field too, then hand you a short list. Your Flask search endpoint is that librarian — focused, efficient, and clear about what fields it searches.
The core abstraction is a search function that takes a query string and returns a list of matches. This function is decoupled from HTTP so you can test it independently and reuse it for CLI scripts or API calls.
How it works step by step
Creating a search endpoint in Flask follows a logical flow. Let's break it down:
1. Set up a minimal Flask app
First, make sure Flask is installed (pip install Flask). Create a file app.py with a simple app and an in-memory list of items (for now) to search through.
2. Create the search route
Define a route like /search that accepts GET requests. Use request.args.get('q', '') to grab the query parameter. This is safer than request.args['q'] because it won't raise a 400 if the parameter is missing.
3. Implement the filtering logic
Write a function that takes the query and returns matching items. For a list of dictionaries, use a list comprehension with a conditional check — for example, check if the query term (lowercased) appears in the item's name or description (also lowercased). This gives case-insensitive substring matching.
4. Return JSON
Use jsonify to serialize the results. Include a count field and maybe a query echo so the client knows what it searched. This makes debugging easier.
5. Test the endpoint
Run the app and hit it with curl or a browser. Try edge cases like empty q, uppercase, and special characters.
Hands-on walkthrough
Now let's build it. We'll start with a simple in-memory dataset, then move to a database example.
Example 1: Search a list of dictionaries
from flask import Flask, request, jsonify
app = Flask(__name__)
# Sample data - in real life this would come from a database
PRODUCTS = [
{"id": 1, "name": "Wireless Mouse", "category": "Electronics"},
{"id": 2, "name": "Mechanical Keyboard", "category": "Electronics"},
{"id": 3, "name": "Yoga Mat", "category": "Fitness"},
{"id": 4, "name": "Running Shoes", "category": "Fitness"},
]
@app.route('/search', methods=['GET'])
def search():
q = request.args.get('q', '').strip()
if not q:
return jsonify({"error": "Missing 'q' parameter"}), 400
q_lower = q.lower()
results = [
p for p in PRODUCTS
if q_lower in p['name'].lower() or q_lower in p['category'].lower()
]
return jsonify({"query": q, "count": len(results), "results": results})
if __name__ == '__main__':
app.run(debug=True)
Expected output when you run curl "http://localhost:5000/search?q=key":
{"count":1, "query":"key", "results":[{"category":"Electronics", "id":2, "name":"Mechanical Keyboard"}]}
Example 2: Search a SQLite database
When your data outgrows a Python list, move to a database. Here's how to create a search endpoint in Flask using SQLite with a parameterized query (never use f-strings for SQL — you'll see why in Troubleshooting).
import sqlite3
from flask import Flask, request, jsonify, g
app = Flask(__name__)
DATABASE = 'products.db'
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
return db
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
@app.route('/search', methods=['GET'])
def search():
q = request.args.get('q', '').strip()
if not q:
return jsonify({"error": "Missing 'q' parameter"}), 400
db = get_db()
cursor = db.execute(
"SELECT * FROM products WHERE name LIKE ? OR category LIKE ?",
(f'%{q}%', f'%{q}%')
)
rows = cursor.fetchall()
# Convert sqlite3.Row to dict for JSON serialization
columns = [d[0] for d in cursor.description]
results = [dict(zip(columns, row)) for row in rows]
return jsonify({"query": q, "count": len(results), "results": results})
Expected behavior: LIKE ? with % wildcards gives partial matching. Parameterized queries prevent SQL injection. This is the pattern you'll use in real projects.
Example 3: When to use a search function
Refactoring the logic into a separate function makes it testable and reusable:
def search_products(q):
q_lower = q.lower()
return [p for p in PRODUCTS if q_lower in p['name'].lower() or q_lower in p['category'].lower()]
@app.route('/search', methods=['GET'])
def search():
q = request.args.get('q', '').strip()
if not q:
return jsonify({"error": "Missing 'q' parameter"}), 400
results = search_products(q)
return jsonify({"query": q, "count": len(results), "results": results})
Now you can unit-test search_products without spinning up a Flask test client.
Compare options / when to choose what
You have several ways to implement filtering. Each fits a different scenario.
| Approach | Best for | Pros | Cons |
|---|---|---|---|
| In-memory list comprehension | Small, static datasets (<1k items) | Simple, fast, no DB setup | Doesn't scale, holds all data in RAM |
SQL LIKE query |
Moderate data, flexibility in filtering | Uses database indexes (if set up), familiar | % wildcards can be slow on large tables without full-text index |
Full-text search (e.g., SQLite FTS5, PostgreSQL tsvector) |
Large datasets, need ranking/relevance | Fast, supports ranking, stemming | More complex to configure and maintain |
| External search service (Elasticsearch, Algolia) | Very large or multi-tenant apps | Scales horizontally, advanced features | Extra infrastructure costs, learning curve |
As a rule of thumb: start with list comprehension for prototypes, move to LIKE when you add a database, then consider full-text or external services only when you hit thousands of rows or need relevance ranking.
Troubleshooting & edge cases
1. Query parameter missing → 400 error
Symptom: curl http://localhost:5000/search returns a Bad Request.
Cause: You used request.args['q'], which raises a KeyError when absent.
Fix: Use .get('q', '') and handle empty strings as shown earlier. Always default to an empty string and return a friendly 400 with a JSON error message.
2. SQL injection from f-strings
Symptom: Someone sends q='; DROP TABLE products; -- and your database gets wiped.
Cause: You wrote f"SELECT * FROM products WHERE name LIKE '%{q}%'". This concatenates raw user input into SQL.
Fix: Always use parameterized queries with ? placeholders. In SQLAlchemy, use text() with :param or ORM methods that bind parameters automatically.
3. Case sensitivity surprises
Symptom: q=Mouse returns nothing even though you have mouse in the data.
Cause: SQLite LIKE is case‑insensitive for ASCII by default, but Python string in is case‑sensitive, and PostgreSQL LIKE is case‑sensitive unless using ILIKE.
Fix: Normalize both sides to lowercase when searching in Python. For databases, test your specific engine's behavior and document it.
4. Empty results vs. missing parameter
Symptom: You return a 200 with empty results when no query is given, but the client can't tell whether it's a valid empty search or a mistake.
Fix: Return a 400 if q is missing or only whitespace. This makes API behavior explicit. For a legitimate query that matches nothing, return 200 with count: 0.
5. Performance on large tables
Symptom: Searches become slower as rows grow (e.g., 10k+).
Cause: LIKE '%term%' can't use a regular index effectively because the wildcard is at the start.
Fix: For large datasets, consider full-text search (SQLite FTS5, PostgreSQL tsvector) or add an index on name if you only search prefixes (LIKE 'term%'). Monitor query plans.
What you learned & what's next
You now know how to create a search endpoint in Flask that is more than a toy. You can: extract query parameters safely, filter in-memory lists or a database, return structured JSON with counts, and avoid the most painful pitfalls like SQL injection and case issues. These skills are the foundation for any search feature in a real application.
To continue on your Python web development path, the next lesson likely covers pagination and filtering collections — how to return large result sets in manageable chunks, add sort order, and combine multiple filters (like category + price). That builds directly on the search endpoint pattern you just mastered: same route structure, but more query parameters and smarter database queries.
Keep practicing — your future users will appreciate fast, correct search results!
Practice recap
Build a /search endpoint on a list of your own (e.g., books or movies). First, filter by title. Then add a second field (author or genre) and test with curl and edge cases like empty q and uppercase queries. Finally, refactor the filtering into a separate function and write a quick test with pytest.
Common mistakes
- Using
request.args['q']directly — crashes with a400if the parameter is missing. Always use.get('q', '')and handle empty strings. - Concatenating user input into SQL strings with f-strings — an open door for SQL injection. Use parameterized queries with
?placeholders. - Forgetting to handle case sensitivity — Python string
inis case-sensitive, SQLiteLIKEis case-insensitive for ASCII. Normalize to lowercase consistently. - Returning a
200with empty results whenqis missing or whitespace — the API can't distinguish between a valid empty search and a missing parameter. Return400for missing/blankq.
Variations
- Use SQLAlchemy ORM's
filter(Model.field.contains(q))instead of raw SQL — safer and database-agnostic. - Add full-text search with SQLite FTS5 or PostgreSQL
tsvectorfor ranking and speed on large datasets. - Consider query parameter filtering (e.g.,
/search?q=term&category=electronics) to combine multiple filters in one endpoint.
Real-world use cases
- E-commerce product search:
/search?q=wireless+mousereturns matching products with names and categories, used in an online storefront. - Blog engine search:
/search?q=flaskfinds articles by title and content, returning a JSON list for an autocomplete widget. - User directory API:
/search?q=janefilters users by name or email, powering an admin panel's member lookup.
Key takeaways
- Always extract query parameters with
request.args.get('q', '')to avoid crashes on missing input. - Implement a dedicated
search_*function to keep filtering logic testable and separate from HTTP handling. - Use parameterized SQL (with placeholders) to prevent injection when searching a database.
- Normalize case for consistent matching between Python and different database engines.
- Follow the filter pipeline pattern: sanitize → filter → return JSON with count.
- Start with simple list filtering, then move to
LIKEand full-text search as your data grows.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.