Full CRUD API (Flask + SQLite)

Flask is one of the lightest web frameworks in the Python ecosystem — you can have a working REST API up in a few minutes. This example builds an "articles" API covering the full CRUD set: list, detail, create, update, and delete, backed by a zero-config SQLite store with CORS enabled. Get it running locally first, then port the same ideas to MySQL/PostgreSQL or to FastAPI/Express. It's a good companion to the Node.js API example if you're comparing backend stacks. Treat this example as a ready-made backend scaffold: clear structure, commented code, and easy to adapt by swapping fields.

1. Install Dependencies

mkdir flask-api && cd flask-api
python3 -m venv venv
source venv/bin/activate
pip install flask flask-cors

2. Main Application

from flask import Flask, request, jsonify
from flask_cors import CORS
import sqlite3
from datetime import datetime

app = Flask(__name__)
CORS(app)  # cross-origin support

DATABASE = 'app.db'


def get_db():
    conn = sqlite3.connect(DATABASE)
    conn.row_factory = sqlite3.Row
    return conn


def init_db():
    with get_db() as db:
        db.execute('''CREATE TABLE IF NOT EXISTS articles (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            content TEXT,
            created_at TEXT DEFAULT (datetime('now'))
        )''')
        db.commit()


init_db()


@app.route('/api/articles', methods=['GET'])
def get_articles():
    with get_db() as db:
        articles = db.execute(
            'SELECT * FROM articles ORDER BY created_at DESC'
        ).fetchall()
    return jsonify([dict(a) for a in articles])


@app.route('/api/articles/<int:id>', methods=['GET'])
def get_article(id):
    with get_db() as db:
        article = db.execute(
            'SELECT * FROM articles WHERE id = ?', (id,)
        ).fetchone()
    if not article:
        return jsonify({'error': 'Article not found'}), 404
    return jsonify(dict(article))


@app.route('/api/articles', methods=['POST'])
def create_article():
    data = request.get_json()
    if not data or not data.get('title'):
        return jsonify({'error': 'Title is required'}), 400

    with get_db() as db:
        cursor = db.execute(
            'INSERT INTO articles (title, content) VALUES (?, ?)',
            (data['title'], data.get('content', ''))
        )
        db.commit()
        article = db.execute(
            'SELECT * FROM articles WHERE id = ?', (cursor.lastrowid,)
        ).fetchone()
    return jsonify(dict(article)), 201


@app.route('/api/articles/<int:id>', methods=['PUT'])
def update_article(id):
    data = request.get_json()
    with get_db() as db:
        cursor = db.execute(
            'UPDATE articles SET title=?, content=? WHERE id=?',
            (data.get('title'), data.get('content'), id)
        )
        db.commit()
        if cursor.rowcount == 0:
            return jsonify({'error': 'Article not found'}), 404
        article = db.execute(
            'SELECT * FROM articles WHERE id = ?', (id,)
        ).fetchone()
    return jsonify(dict(article))


@app.route('/api/articles/<int:id>', methods=['DELETE'])
def delete_article(id):
    with get_db() as db:
        cursor = db.execute('DELETE FROM articles WHERE id=?', (id,))
        db.commit()
        if cursor.rowcount == 0:
            return jsonify({'error': 'Article not found'}), 404
    return jsonify({'message': 'Deleted'})


if __name__ == '__main__':
    app.run(debug=True, port=5000)

3. Run and Test

python3 app.py
# Server running at http://localhost:5000

# Test the API
curl http://localhost:5000/api/articles
curl -X POST -H "Content-Type: application/json" \
  -d '{"title":"Hello Flask","content":"API example"}' \
  http://localhost:5000/api/articles

4. Production Deployment: Skip the Built-in Server

Flask's dev server (app.run) is for local debugging only — it's single-process with no concurrency, and exposing it to the public is asking for trouble. Use gunicorn in production:

pip install gunicorn
gunicorn -w 4 -b 127.0.0.1:8000 app:app

Four workers is a common starting point for small and mid-sized sites. Pair it with an Nginx reverse proxy that forwards 80/443 traffic to port 8000 and you have a production-ready API. You can reuse the Nginx example from the backend integration guide.

5. Error Handling: Make Status Codes Mean Something

The example returns explicit status codes and JSON errors for 400 and 404. In production, standardize the error shape so the frontend can branch on code instead of parsing human text:

{
  "error": {
    "code": "ARTICLE_NOT_FOUND",
    "message": "Article not found"
  }
}

Also validate inputs on write endpoints: an empty title should return 400, not bubble up a database exception.

6. Reading the Code

  • Parameterized queries: every SQL statement passes values via ? placeholders instead of string concatenation. This is the most basic and effective defense against SQL injection, and any code that concatenates user input into SQL should be rejected in review.
  • row_factory: sqlite3.Row lets you access columns by name, convert to dict, and serialize straight to JSON — no manual mapping.
  • created_at default: generating the timestamp in the database layer with datetime('now') avoids timezone drift between app instances.
  • CORS: cross-origin is the norm in split frontend/backend setups; flask-cors enables it in one line, but narrow it to an allowlist in production.
  • Status codes: 201 on create, 200 on delete, 404 on missing, 400 on bad input — clear semantics make frontend handling predictable.

7. Verifying Core Logic with pytest

import pytest
from app import app

@pytest.fixture
def client():
    app.config['TESTING'] = True
    return app.test_client()

def test_create_and_get(client):
    r = client.post('/api/articles', json={'title': 'Test'})
    assert r.status_code == 201
    rid = r.get_json()['id']
    r2 = client.get(f'/api/articles/{rid}')
    assert r2.status_code == 200

def test_missing_title(client):
    r = client.post('/api/articles', json={})
    assert r.status_code == 400

Automated tests catch "I broke the API" before it merges, instead of finding out during frontend integration.

8. Frequently Asked Questions

Why SQLite? Zero config and a single file make it ideal for prototypes and low-traffic projects. When you need multi-process writes or higher concurrency, move to MySQL/PostgreSQL with a thin data-access layer.

Cross-origin errors? CORS(app) allows all origins by default. In production, use an allowlist: CORS(app, resources={r"/api/*": {"origins": ["https://your-site.com"]}}).

How do I add pagination? Add limit and offset to the list endpoint and return the total count — enough to power frontend pagination or infinite scroll.

Should I add authentication? If the API isn't fully public, yes. The simplest path is issuing JWTs: the client sends Authorization: Bearer <token> and the server validates it. In the Flask ecosystem, flask-jwt-extended handles this cleanly.

A Real Integration Scenario: Calling the API from a Page

Say your static site (for example, a showcase page built with an AI site builder) needs to display a "company news" list. The frontend just calls /api/articles:

fetch('https://api.example.com/api/articles')
  .then(r => r.json())
  .then(list => {
    document.querySelector('#news').innerHTML = list
      .slice(0, 5)
      .map(a => `<li><a href="/article/${a.id}">${a.title}</a></li>`)
      .join('');
  });

Two notes: put the API base URL in config rather than hardcoding it, and turn off debug=True in production — otherwise error stack traces leak to visitors.

9. When to Choose Flask

Flask fits small and mid-sized projects, prototypes, internal tools, and AI-generated site backends. It's light, quick to pick up, and well documented; the trade-off is that async and high concurrency aren't its strengths. If you expect IO-heavy concurrency (chat, push, long-lived connections), look at FastAPI (native async) or Node.js.

Scenario Recommendation Why
Small-team admin/backend Flask Lightweight, simple, Python ecosystem
High-concurrency IO, streaming FastAPI Native async, better throughput
Frontend team leads, isomorphic Node.js/Express One language end to end
Complex business, fast iteration Django Built-in ORM, admin, auth

For most web-building scenarios, Flask behind gunicorn is plenty. Migrate when you hit a real bottleneck — don't reach for the heaviest stack on day one.

Summary

The CRUD in this example is a reusable skeleton: parameterized queries, JSON output, CORS, status codes, and tests. Swap the business fields, swap the database, swap the language — the pattern stays the same. Build your own API template from this structure and add error shapes, pagination, and auth incrementally; it beats starting from scratch every time. When in doubt, check the official docs and the migration notes for your version rather than copying an old tutorial.

References: Flask docs https://flask.palletsprojects.com/ , gunicorn docs https://docs.gunicorn.org/

Features

  • ✅ Full CRUD (Create, Read, Update, Delete)
  • ✅ SQLite database (zero configuration)
  • ✅ Parameterized queries (SQL injection prevention)
  • ✅ CORS support
  • ✅ JSON input/output
  • ✅ Error handling with HTTP status codes