Building a Contact Form Backend with FastAPI and Discord Integration

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-dotenv
pip <span>install </span>fastapi uvicorn httpx python-dotenv
pip 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_here
FASTAPI_DISCORD_WEBHOOK_URL=your_discord_webhook_url_here
FASTAPI_DISCORD_WEBHOOK_URL=your_discord_webhook_url_here

Enter fullscreen mode Exit fullscreen mode

How It Works

  1. 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
  2. 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
  3. 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
  4. 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

  1. CORS: Only allow domains that actually need to access your API
  2. Environment Variables: Keep sensitive data like webhook URLs in environment variables
  3. Input Validation: Use Pydantic models to validate all incoming data
  4. 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

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
I lose, I lose, but I never give up.
我输过,我败过,但我从未放弃过
评论 抢沙发

请登录后发表评论

    暂无评论内容