Pro Tip

Follow along with the code samples by typing them into your editor to better understand how Expressify works.

Browse examples

Getting Started with Expressify

Everything you need to know to get up and running with Expressify - the Express.js-inspired framework for Python.

Installation

Expressify requires Python 3.7 or higher. You can install it using pip:

Terminal
pip install expressify

Or install from source:

Terminal
git clone https://github.com/itsdhruvrawat/expressify.git
cd expressify
pip install -e .

Note

Make sure to have the latest version of pip installed to avoid any compatibility issues.

Your First App

Let's create a simple "Hello, World!" application with Expressify:

app.py
from expressify import expressify

# Create a new application
app = expressify()

# Define a route for the home page
@app.get('/')
def home(req, res):
    res.send('Hello, World!')

# Start the server
if __name__ == '__main__':
    app.listen(port=3000, hostname='127.0.0.1')  # Defaults to 127.0.0.1:3000

Save this code to a file named app.py and run it:

Terminal
python app.py
Output
Expressify server running on http://127.0.0.1:3000
Press CTRL+C to quit
Browser icon

See it in action

Open your web browser and navigate to http://127.0.0.1:3000 to see "Hello, World!" displayed.

Development Server with Hot Reloading

Development Server

Expressify includes a development server with hot reloading capabilities to make your development workflow more efficient. When you run your application with dev.py, it automatically detects file changes and restarts your application in real-time.

šŸ”„ Expressify development server starting...
šŸ“ Application: app.py
šŸš€ Starting application: app.py
šŸ“¤ Server running at http://127.0.0.1:3000
šŸ“¤ Press Ctrl+C to stop the server
...
šŸ”„ File changed: app.py
šŸ”„ Reloading application...
šŸ›‘ Stopping current process...
šŸš€ Starting application: app.py
šŸ“¤ Server running at http://127.0.0.1:3000
...

Behind the scenes, the development server uses Python's watchdog package to monitor file changes and automatically restarts your application when any Python file is modified.

Full Source Code

If you want to use the development server in your own project without installing the full Expressify package, you can copy the following code into a file named dev.py:

#!/usr/bin/env python3
"""
Expressify Development Server with Hot Reloading

This script provides a development server with automatic reloading
when source files change. It monitors the current directory and restarts
the application when any Python file changes.

Usage:
    python dev.py app.py
    python dev.py examples/01-hello-world/app.py
"""

import argparse
import importlib.util
import os
import signal
import subprocess
import sys
import time
from pathlib import Path
from types import ModuleType
from typing import Any, List, Optional

try:
    from watchdog.events import FileSystemEventHandler
    from watchdog.observers import Observer
except ImportError:
    print("watchdog package is required for dev server. Install it with:")
    print("pip install watchdog")
    sys.exit(1)

class AppReloader(FileSystemEventHandler):
    """Watches for file changes and reloads the application."""
    
    def __init__(self, app_path: str, watch_dirs: List[str] = None):
        self.app_path = app_path
        self.watch_dirs = watch_dirs or ['.']
        self.process: Optional[subprocess.Popen] = None
        self.observer = Observer()
        self.module_name = os.path.basename(app_path).replace('.py', '')
        self.module: Optional[ModuleType] = None
        self.last_reload = 0
        self.reloading = False
        
        # Handle SIGINT (Ctrl+C) gracefully
        signal.signal(signal.SIGINT, self.handle_sigint)
            
    def start(self):
        """Start the development server with hot reloading."""
        print(f"šŸ”„ Expressify development server starting...")
        print(f"šŸ“ Application: {self.app_path}")
        
        # Start watching for file changes
        for watch_dir in self.watch_dirs:
            self.observer.schedule(self, watch_dir, recursive=True)
        self.observer.start()
        
        try:
            self.load_app()
            while True:
                time.sleep(1)
        except KeyboardInterrupt:
            self.stop()
    
    def stop(self):
        """Stop the reloader and clean up resources."""
        print("\nšŸ›‘ Shutting down development server...")
        if self.process:
            self.kill_process()
        self.observer.stop()
        self.observer.join()
        print("šŸ‘‹ Server stopped")
        
    def handle_sigint(self, sig, frame):
        """Handle SIGINT (Ctrl+C) signal."""
        self.stop()
        sys.exit(0)
        
    def on_modified(self, event):
        """Called when a file is modified."""
        if event.is_directory:
            return
            
        # Only reload on Python file changes
        if not event.src_path.endswith('.py'):
            return
            
        # Avoid multiple simultaneous reloads
        current_time = time.time()
        if current_time - self.last_reload < 1 or self.reloading:
            return
            
        self.last_reload = current_time
        self.reloading = True
        print(f"\nšŸ”„ File changed: {os.path.basename(event.src_path)}")
        self.reload_app()
        self.reloading = False
    
    def load_app(self):
        """Load or reload the application."""
        if self.process:
            self.kill_process()
        
        print(f"šŸš€ Starting application: {self.app_path}")
        
        # Start the application as a subprocess
        try:
            self.process = subprocess.Popen(
                [sys.executable, self.app_path],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                bufsize=1  # Line buffered
            )
            
            # Start threads to read stdout and stderr
            self.start_output_reader(self.process.stdout, "šŸ“¤")
            self.start_output_reader(self.process.stderr, "āš ļø")
            
        except Exception as e:
            print(f"āŒ Failed to start application: {e}")
            self.process = None
    
    def reload_app(self):
        """Reload the application after changes."""
        print("šŸ”„ Reloading application...")
        self.load_app()
    
    def kill_process(self):
        """Kill the current process if it exists."""
        if self.process:
            print("šŸ›‘ Stopping current process...")
            try:
                # Try to terminate gracefully first
                self.process.terminate()
                # Give it some time to terminate
                for _ in range(5):
                    if self.process.poll() is not None:
                        break
                    time.sleep(0.1)
                    
                # If still running, force kill
                if self.process.poll() is None:
                    self.process.kill()
                    self.process.wait()
            except Exception as e:
                print(f"āŒ Error stopping process: {e}")
            
            self.process = None
    
    def start_output_reader(self, pipe, prefix):
        """Start a thread to read output from the subprocess."""
        def reader():
            for line in iter(pipe.readline, ''):
                if line.strip():
                    print(f"{prefix} {line.rstrip()}")
            
        import threading
        thread = threading.Thread(target=reader)
        thread.daemon = True
        thread.start()

def find_watch_dirs(app_path: str) -> List[str]:
    """Find directories to watch for changes."""
    app_dir = os.path.dirname(os.path.abspath(app_path))
    
    # Always watch the directory of the app
    watch_dirs = [app_dir]
    
    # If it's an examples subdirectory, also watch the core expressify package
    if app_dir.startswith(os.path.join(os.getcwd(), 'examples')):
        expressify_dir = os.path.join(os.getcwd(), 'expressify')
        if os.path.exists(expressify_dir):
            watch_dirs.append(expressify_dir)
    
    return watch_dirs

def main():
    """Main entry point for the development server."""
    parser = argparse.ArgumentParser(description="Expressify Development Server with Hot Reloading")
    parser.add_argument('app_path', help="Path to the main application file")
    args = parser.parse_args()
    
    # Ensure the app file exists
    if not os.path.exists(args.app_path):
        print(f"āŒ Error: App file not found: {args.app_path}")
        sys.exit(1)
    
    # Find directories to watch
    watch_dirs = find_watch_dirs(args.app_path)
    
    # Start the reloader
    reloader = AppReloader(args.app_path, watch_dirs)
    reloader.start()

if __name__ == "__main__":
    main()

To use this script:

  1. Copy the code above into a file named dev.py in your project
  2. Install the watchdog package: pip install watchdog
  3. Make the script executable: chmod +x dev.py (Linux/macOS only)
  4. Run your application with: python dev.py app.py

Middleware

Middleware functions are functions that have access to the request object, response object, and the next middleware function in the application's request-response cycle. Middleware can:

  • Execute code during the request-response cycle
  • Modify the request and response objects
  • End the request-response cycle
  • Call the next middleware in the stack
Global Middleware
Route-Specific Middleware

Global middleware is applied to all routes in your application. This is useful for functionality like logging, authentication, or setting common headers.

Using Decorator Syntax

from expressify import Expressify

app = Expressify()

@app.use
def logger_middleware(req, res, next):
    """Log request information"""
    print(f"{req.method} {req.path}")
    
    # Call the next middleware/route handler
    result = next()
    
    # Log the response status code
    print(f"Response: {res.status_code}")
    
    # Return the result
    return result

@app.get('/')
def home(req, res):
    return res.send('Home page')

if __name__ == '__main__':
    app.listen(port=3000)

Using Express-like Syntax

from expressify import expressify

app = expressify()

def logger_middleware(req, res, next):
    print(f"{req.method} {req.path}")
    next()

# Apply middleware globally
app.use(logger_middleware)

def home(req, res):
    res.send('Home page')

app.get('/', home)

if __name__ == '__main__':
    app.listen(port=3000)

Middleware Chain

Multiple middleware functions can be chained together. Each middleware has access to the request and response objects, and can either pass control to the next middleware using next() or terminate the request-response cycle.

from expressify import Expressify
import time
import json

app = Expressify()

# Define multiple middleware functions
@app.use
def logger_middleware(req, res, next):
    """Log request information and timing data"""
    start_time = time.time()
    print(f"Request: {req.method} {req.path}")
    result = next()
    duration = (time.time() - start_time) * 1000  # Convert to ms
    print(f"Response: {res.status_code} - {duration:.2f}ms")
    return result

@app.use
def cors_middleware(req, res, next):
    """Add CORS headers to enable cross-origin requests"""
    res.set_header('Access-Control-Allow-Origin', '*')
    res.set_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
    res.set_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
    
    if req.method == 'OPTIONS':
        return res.status(204).send()
    return next()

@app.use
def json_parser_middleware(req, res, next):
    """Parse JSON request bodies"""
    if 'application/json' in req.get_header('content-type', ''):
        try:
            if req.body:
                req.json = json.loads(req.body)
        except json.JSONDecodeError:
            return res.status(400).json({
                'error': 'Invalid JSON',
                'message': 'Failed to parse request body as JSON'
            })
    return next()

@app.get('/')
def home(req, res):
    return res.send('Hello from Expressify!')

@app.post('/api/data')
def create_data(req, res):
    # The JSON parser middleware will have already parsed the JSON body
    if hasattr(req, 'json'):
        return res.status(201).json({
            'message': 'Data received successfully',
            'data': req.json
        })
    else:
        return res.status(400).json({
            'error': 'No data provided',
            'message': 'Request body should contain JSON data'
        })

if __name__ == '__main__':
    app.listen(port=3000)

Key Concept

Middleware execution follows the order in which they are defined. Global middleware defined with app.use() runs first, followed by route-specific middleware, and finally the route handler.

Common Middleware Use Cases

Authentication

Verify user identity and protect routes that require authentication. Authentication middleware can check for tokens, sessions, or other credentials.

Logging

Record request information, response times, and other metrics. Logging middleware can be valuable for debugging and monitoring application performance.

CORS

Handle Cross-Origin Resource Sharing to control which domains can access your API. CORS middleware sets appropriate headers for cross-domain requests.

Compression

Compress HTTP responses to reduce bandwidth and improve loading times. Compression middleware typically uses gzip or other algorithms to reduce payload size.

Template Rendering

Expressify includes built-in support for Jinja2 templates:

import os
from expressify import expressify

app = expressify()

# Set up the template engine
app.set('view engine', 'jinja2')
app.set('views', os.path.join(os.path.dirname(__file__), 'views'))

@app.get('/')
def home(req, res):
    # Render the index.html template with data
    res.render('index.html', {
        'title': 'Expressify Example',
        'message': 'Welcome to Expressify!',
        'items': ['Item 1', 'Item 2', 'Item 3']
    })

Create a template file views/index.html:

<!DOCTYPE html>
<html>
<head>
    <title>{{ title }}</title>
</head>
<body>
    <h1>{{ message }}</h1>
    <ul>
        {% for item in items %}
            <li>{{ item }}</li>
        {% endfor %}
    </ul>
</body>
</html>

Serving Static Files

Expressify makes it easy to serve static files like CSS, JavaScript, and images:

from expressify import expressify
from expressify.lib.middleware import Middleware
import os

app = expressify()

# Serve static files from the 'public' directory
app.use('/static', Middleware.static(os.path.join(os.path.dirname(__file__), 'public')))

# Now you can access files at /static/css/style.css, /static/js/script.js, etc.

Next Steps

Ready to build something amazing?

You've learned the basics of Expressify. Now it's time to build your own application!

Explore Examples