Flask search endpoint

Create a search endpoint in Flask — Python web development tutorial.

Focus: create a search endpoint in flask

Sponsored

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:

  1. Receive the term (or terms) from the query string.
  2. Sanitize it — strip whitespace, decide on case sensitivity, maybe split into words.
  3. Filter your data source using the sanitized term.
  4. 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 a 400 if 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 in is case-sensitive, SQLite LIKE is case-insensitive for ASCII. Normalize to lowercase consistently.
  • Returning a 200 with empty results when q is missing or whitespace — the API can't distinguish between a valid empty search and a missing parameter. Return 400 for missing/blank q.

Variations

  1. Use SQLAlchemy ORM's filter(Model.field.contains(q)) instead of raw SQL — safer and database-agnostic.
  2. Add full-text search with SQLite FTS5 or PostgreSQL tsvector for ranking and speed on large datasets.
  3. 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+mouse returns matching products with names and categories, used in an online storefront.
  • Blog engine search: /search?q=flask finds articles by title and content, returning a JSON list for an autocomplete widget.
  • User directory API: /search?q=jane filters 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 LIKE and full-text search as your data grows.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.