In this tutorial, we’ll build a secure backend API using FastAPI that handles contact form submissions and forwards them to a Discord channel using webhooks. We’ll also cover how to properly set up CORS to allow requests from specific domains.
Prerequisites
- Python 3.11+
- FastAPI
- httpx (for making async HTTP requests)
- A Discord webhook URL
Step 1: Setting Up the Project
First, create a new directory for your project and install the required dependencies:
pip <span>install </span>fastapi uvicorn httpx python-dotenvpip <span>install </span>fastapi uvicorn httpx python-dotenvpip install fastapi uvicorn httpx python-dotenv
Enter fullscreen mode Exit fullscreen mode
Step 2: Creating the FastAPI Application
Create a new file called main.py
:
<span>import</span> <span>os</span><span>from</span> <span>fastapi</span> <span>import</span> <span>FastAPI</span><span>,</span> <span>HTTPException</span><span>from</span> <span>fastapi.middleware.cors</span> <span>import</span> <span>CORSMiddleware</span><span>from</span> <span>pydantic</span> <span>import</span> <span>BaseModel</span><span>import</span> <span>httpx</span><span>app</span> <span>=</span> <span>FastAPI</span><span>()</span><span># Configure CORS </span><span>app</span><span>.</span><span>add_middleware</span><span>(</span><span>CORSMiddleware</span><span>,</span><span>allow_origins</span><span>=</span><span>[</span><span>"</span><span>https://vicentereyes.org</span><span>"</span><span>,</span><span>"</span><span>https://www.vicentereyes.org</span><span>"</span><span>],</span><span>allow_credentials</span><span>=</span><span>True</span><span>,</span><span>allow_methods</span><span>=</span><span>[</span><span>"</span><span>*</span><span>"</span><span>],</span><span>allow_headers</span><span>=</span><span>[</span><span>"</span><span>*</span><span>"</span><span>],</span><span>)</span><span>import</span> <span>os</span> <span>from</span> <span>fastapi</span> <span>import</span> <span>FastAPI</span><span>,</span> <span>HTTPException</span> <span>from</span> <span>fastapi.middleware.cors</span> <span>import</span> <span>CORSMiddleware</span> <span>from</span> <span>pydantic</span> <span>import</span> <span>BaseModel</span> <span>import</span> <span>httpx</span> <span>app</span> <span>=</span> <span>FastAPI</span><span>()</span> <span># Configure CORS </span><span>app</span><span>.</span><span>add_middleware</span><span>(</span> <span>CORSMiddleware</span><span>,</span> <span>allow_origins</span><span>=</span><span>[</span> <span>"</span><span>https://vicentereyes.org</span><span>"</span><span>,</span> <span>"</span><span>https://www.vicentereyes.org</span><span>"</span> <span>],</span> <span>allow_credentials</span><span>=</span><span>True</span><span>,</span> <span>allow_methods</span><span>=</span><span>[</span><span>"</span><span>*</span><span>"</span><span>],</span> <span>allow_headers</span><span>=</span><span>[</span><span>"</span><span>*</span><span>"</span><span>],</span> <span>)</span>import os from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel import httpx app = FastAPI() # Configure CORS app.add_middleware( CORSMiddleware, allow_origins=[ "https://vicentereyes.org", "https://www.vicentereyes.org" ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )
Enter fullscreen mode Exit fullscreen mode
Step 3: Define the Data Model
We’ll use Pydantic to define our form data structure:
<span>class</span> <span>FormData</span><span>(</span><span>BaseModel</span><span>):</span><span>name</span><span>:</span> <span>str</span><span>email</span><span>:</span> <span>str</span><span>message</span><span>:</span> <span>str</span><span>service</span><span>:</span> <span>str</span><span>companyName</span><span>:</span> <span>str</span><span>companyUrl</span><span>:</span> <span>str</span><span>class</span> <span>FormData</span><span>(</span><span>BaseModel</span><span>):</span> <span>name</span><span>:</span> <span>str</span> <span>email</span><span>:</span> <span>str</span> <span>message</span><span>:</span> <span>str</span> <span>service</span><span>:</span> <span>str</span> <span>companyName</span><span>:</span> <span>str</span> <span>companyUrl</span><span>:</span> <span>str</span>class FormData(BaseModel): name: str email: str message: str service: str companyName: str companyUrl: str
Enter fullscreen mode Exit fullscreen mode
Step 4: Create the Submission Endpoint
Add the endpoint that handles form submissions:
<span>@app.post</span><span>(</span><span>"</span><span>/submit/</span><span>"</span><span>)</span><span>@app.post</span><span>(</span><span>"</span><span>/submit</span><span>"</span><span>)</span> <span># Handle both with and without trailing slash </span><span>async</span> <span>def</span> <span>submit_form</span><span>(</span><span>form_data</span><span>:</span> <span>FormData</span><span>):</span><span>try</span><span>:</span><span># Format the message for Discord </span> <span>message_content</span> <span>=</span> <span>{</span><span>"</span><span>content</span><span>"</span><span>:</span> <span>f</span><span>"</span><span>New form submission: </span><span>\n</span><span>"</span><span>f</span><span>"</span><span>**Name:** </span><span>{</span><span>form_data</span><span>.</span><span>name</span><span>}</span><span>\n</span><span>"</span><span>f</span><span>"</span><span>**Email:** </span><span>{</span><span>form_data</span><span>.</span><span>email</span><span>}</span><span>\n</span><span>"</span><span>f</span><span>"</span><span>**Message:** </span><span>{</span><span>form_data</span><span>.</span><span>message</span><span>}</span><span>\n</span><span>"</span><span>f</span><span>"</span><span>**Service:** </span><span>{</span><span>form_data</span><span>.</span><span>service</span><span>}</span><span>\n</span><span>"</span><span>f</span><span>"</span><span>**Company Name:** </span><span>{</span><span>form_data</span><span>.</span><span>companyName</span><span>}</span><span>\n</span><span>"</span><span>f</span><span>"</span><span>**Company URL:** </span><span>{</span><span>form_data</span><span>.</span><span>companyUrl</span><span>}</span><span>"</span><span>}</span><span># Send to Discord webhook </span> <span>async</span> <span>with</span> <span>httpx</span><span>.</span><span>AsyncClient</span><span>()</span> <span>as</span> <span>client</span><span>:</span><span>response</span> <span>=</span> <span>await</span> <span>client</span><span>.</span><span>post</span><span>(</span><span>DISCORD_WEBHOOK_URL</span><span>,</span> <span>json</span><span>=</span><span>message_content</span><span>)</span><span>if</span> <span>response</span><span>.</span><span>status_code</span> <span>!=</span> <span>204</span><span>:</span><span>raise</span> <span>HTTPException</span><span>(</span><span>status_code</span><span>=</span><span>response</span><span>.</span><span>status_code</span><span>,</span><span>detail</span><span>=</span><span>"</span><span>Failed to send message to Discord</span><span>"</span><span>)</span><span>return</span> <span>{</span><span>"</span><span>message</span><span>"</span><span>:</span> <span>"</span><span>Form data sent to Discord successfully</span><span>"</span><span>}</span><span>except</span> <span>Exception</span> <span>as</span> <span>e</span><span>:</span><span>raise</span> <span>HTTPException</span><span>(</span><span>status_code</span><span>=</span><span>500</span><span>,</span> <span>detail</span><span>=</span><span>str</span><span>(</span><span>e</span><span>))</span><span>@app.post</span><span>(</span><span>"</span><span>/submit/</span><span>"</span><span>)</span> <span>@app.post</span><span>(</span><span>"</span><span>/submit</span><span>"</span><span>)</span> <span># Handle both with and without trailing slash </span><span>async</span> <span>def</span> <span>submit_form</span><span>(</span><span>form_data</span><span>:</span> <span>FormData</span><span>):</span> <span>try</span><span>:</span> <span># Format the message for Discord </span> <span>message_content</span> <span>=</span> <span>{</span> <span>"</span><span>content</span><span>"</span><span>:</span> <span>f</span><span>"</span><span>New form submission: </span><span>\n</span><span>"</span> <span>f</span><span>"</span><span>**Name:** </span><span>{</span><span>form_data</span><span>.</span><span>name</span><span>}</span><span>\n</span><span>"</span> <span>f</span><span>"</span><span>**Email:** </span><span>{</span><span>form_data</span><span>.</span><span>email</span><span>}</span><span>\n</span><span>"</span> <span>f</span><span>"</span><span>**Message:** </span><span>{</span><span>form_data</span><span>.</span><span>message</span><span>}</span><span>\n</span><span>"</span> <span>f</span><span>"</span><span>**Service:** </span><span>{</span><span>form_data</span><span>.</span><span>service</span><span>}</span><span>\n</span><span>"</span> <span>f</span><span>"</span><span>**Company Name:** </span><span>{</span><span>form_data</span><span>.</span><span>companyName</span><span>}</span><span>\n</span><span>"</span> <span>f</span><span>"</span><span>**Company URL:** </span><span>{</span><span>form_data</span><span>.</span><span>companyUrl</span><span>}</span><span>"</span> <span>}</span> <span># Send to Discord webhook </span> <span>async</span> <span>with</span> <span>httpx</span><span>.</span><span>AsyncClient</span><span>()</span> <span>as</span> <span>client</span><span>:</span> <span>response</span> <span>=</span> <span>await</span> <span>client</span><span>.</span><span>post</span><span>(</span><span>DISCORD_WEBHOOK_URL</span><span>,</span> <span>json</span><span>=</span><span>message_content</span><span>)</span> <span>if</span> <span>response</span><span>.</span><span>status_code</span> <span>!=</span> <span>204</span><span>:</span> <span>raise</span> <span>HTTPException</span><span>(</span><span>status_code</span><span>=</span><span>response</span><span>.</span><span>status_code</span><span>,</span> <span>detail</span><span>=</span><span>"</span><span>Failed to send message to Discord</span><span>"</span><span>)</span> <span>return</span> <span>{</span><span>"</span><span>message</span><span>"</span><span>:</span> <span>"</span><span>Form data sent to Discord successfully</span><span>"</span><span>}</span> <span>except</span> <span>Exception</span> <span>as</span> <span>e</span><span>:</span> <span>raise</span> <span>HTTPException</span><span>(</span><span>status_code</span><span>=</span><span>500</span><span>,</span> <span>detail</span><span>=</span><span>str</span><span>(</span><span>e</span><span>))</span>@app.post("/submit/") @app.post("/submit") # Handle both with and without trailing slash async def submit_form(form_data: FormData): try: # Format the message for Discord message_content = { "content": f"New form submission: \n" f"**Name:** {form_data.name}\n" f"**Email:** {form_data.email}\n" f"**Message:** {form_data.message}\n" f"**Service:** {form_data.service}\n" f"**Company Name:** {form_data.companyName}\n" f"**Company URL:** {form_data.companyUrl}" } # Send to Discord webhook async with httpx.AsyncClient() as client: response = await client.post(DISCORD_WEBHOOK_URL, json=message_content) if response.status_code != 204: raise HTTPException(status_code=response.status_code, detail="Failed to send message to Discord") return {"message": "Form data sent to Discord successfully"} except Exception as e: raise HTTPException(status_code=500, detail=str(e))
Enter fullscreen mode Exit fullscreen mode
Step 5: Environment Configuration
Create a .env
file to store your Discord webhook URL:
FASTAPI_DISCORD_WEBHOOK_URL=your_discord_webhook_url_hereFASTAPI_DISCORD_WEBHOOK_URL=your_discord_webhook_url_hereFASTAPI_DISCORD_WEBHOOK_URL=your_discord_webhook_url_here
Enter fullscreen mode Exit fullscreen mode
How It Works
-
CORS Configuration:
- The
CORSMiddleware
is configured to only accept requests from specified domains - This is crucial for security as it prevents unauthorized domains from accessing your API
- The middleware is set up to allow all HTTP methods and headers while maintaining origin restrictions
- The
-
Data Validation:
- The
FormData
Pydantic model ensures that all incoming data matches the expected structure - If any required fields are missing or have incorrect types, FastAPI automatically returns a validation error
- The
-
Discord Integration:
- When a form is submitted, the data is formatted into a Discord message
- The message is sent to a Discord channel using a webhook URL
- We use
httpx
for making async HTTP requests to Discord’s API
-
Error Handling:
- The endpoint is wrapped in a try-catch block to handle potential errors
- If Discord’s webhook fails, we return an appropriate HTTP error
- Any other exceptions are caught and returned as 500 Internal Server errors
Running the Application
Start the server with:
uvicorn main:app <span>--reload</span>uvicorn main:app <span>--reload</span>uvicorn main:app --reload
Enter fullscreen mode Exit fullscreen mode
The API will be available at http://localhost:8000
.
Security Considerations
- CORS: Only allow domains that actually need to access your API
- Environment Variables: Keep sensitive data like webhook URLs in environment variables
- Input Validation: Use Pydantic models to validate all incoming data
- Error Handling: Never expose internal error details to clients
Frontend Integration
To use this API from your frontend, make sure your requests include the correct headers and match the expected data structure:
<span>const</span> <span>response</span> <span>=</span> <span>await</span> <span>fetch</span><span>(</span><span>'</span><span>your_api_url/submit</span><span>'</span><span>,</span> <span>{</span><span>method</span><span>:</span> <span>'</span><span>POST</span><span>'</span><span>,</span><span>headers</span><span>:</span> <span>{</span><span>'</span><span>Content-Type</span><span>'</span><span>:</span> <span>'</span><span>application/json</span><span>'</span><span>,</span><span>},</span><span>body</span><span>:</span> <span>JSON</span><span>.</span><span>stringify</span><span>({</span><span>name</span><span>:</span> <span>'</span><span>John Doe</span><span>'</span><span>,</span><span>email</span><span>:</span> <span>'</span><span>john@example.com</span><span>'</span><span>,</span><span>message</span><span>:</span> <span>'</span><span>Hello!</span><span>'</span><span>,</span><span>service</span><span>:</span> <span>'</span><span>Consulting</span><span>'</span><span>,</span><span>companyName</span><span>:</span> <span>'</span><span>Example Corp</span><span>'</span><span>,</span><span>companyUrl</span><span>:</span> <span>'</span><span>https://example.com</span><span>'</span><span>})</span><span>});</span><span>const</span> <span>response</span> <span>=</span> <span>await</span> <span>fetch</span><span>(</span><span>'</span><span>your_api_url/submit</span><span>'</span><span>,</span> <span>{</span> <span>method</span><span>:</span> <span>'</span><span>POST</span><span>'</span><span>,</span> <span>headers</span><span>:</span> <span>{</span> <span>'</span><span>Content-Type</span><span>'</span><span>:</span> <span>'</span><span>application/json</span><span>'</span><span>,</span> <span>},</span> <span>body</span><span>:</span> <span>JSON</span><span>.</span><span>stringify</span><span>({</span> <span>name</span><span>:</span> <span>'</span><span>John Doe</span><span>'</span><span>,</span> <span>email</span><span>:</span> <span>'</span><span>john@example.com</span><span>'</span><span>,</span> <span>message</span><span>:</span> <span>'</span><span>Hello!</span><span>'</span><span>,</span> <span>service</span><span>:</span> <span>'</span><span>Consulting</span><span>'</span><span>,</span> <span>companyName</span><span>:</span> <span>'</span><span>Example Corp</span><span>'</span><span>,</span> <span>companyUrl</span><span>:</span> <span>'</span><span>https://example.com</span><span>'</span> <span>})</span> <span>});</span>const response = await fetch('your_api_url/submit', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: 'John Doe', email: 'john@example.com', message: 'Hello!', service: 'Consulting', companyName: 'Example Corp', companyUrl: 'https://example.com' }) });
Enter fullscreen mode Exit fullscreen mode
Conclusion
This setup provides a secure and efficient way to handle contact form submissions and forward them to Discord. The use of FastAPI makes the code clean and maintainable, while proper CORS configuration ensures security. The async nature of the application means it can handle multiple submissions efficiently without blocking.
Code: https://github.com/reyesvicente/Portfolio-Contact-Form-Backend
Live: https://vicentereyes.org/contact
原文链接:Building a Contact Form Backend with FastAPI and Discord Integration
暂无评论内容