Skip to main content

Overview

MkDowner uses a centralized API service to communicate with the Python backend. The service handles file uploads, error handling, and response processing.

API Service

Location

src/services/api.ts

Full Implementation

const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;

export const uploadFiles = async (files: FileList): Promise<Blob> => {
  try {
    const formData = new FormData();
    
    for (let i = 0; i < files.length; i++) {
      formData.append('files', files[i]);
    }

    const response = await fetch(`${API_BASE_URL}/upload`, {
      method: 'POST',
      body: formData,
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const contentType = response.headers.get('content-type');
    if (contentType && contentType.includes('application/json')) {
      const errorData = await response.json();
      throw new Error(errorData.error || 'Server returned JSON instead of file');
    }

    return response.blob();
  } catch (error) {
    if (error instanceof TypeError && error.message.includes('fetch')) {
      throw new Error('Cannot connect to server. Make sure backend is running on ' + API_BASE_URL);
    }
    throw error;
  }
};

Environment Configuration

The API service reads the backend URL from environment variables:
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;

Configuration File

Create a .env file in the project root:
VITE_API_BASE_URL=http://localhost:5001
The backend must be running on the configured URL before making API calls. The service will throw a connection error if the backend is unreachable.

uploadFiles Function

Signature

uploadFiles(files: FileList): Promise<Blob>

Parameters

ParameterTypeDescription
filesFileListFiles to upload (from <input type="file">)

Return Value

Returns a Promise<Blob> containing the converted file(s):
  • Single file: Markdown file (.md)
  • Multiple files: ZIP archive containing all converted files

Implementation Details

1. FormData Construction

The function creates a FormData object and appends all files:
const formData = new FormData();

for (let i = 0; i < files.length; i++) {
  formData.append('files', files[i]);
}
All files are appended with the same key 'files' to support multiple file uploads. The backend should expect an array of files under this key.

2. HTTP Request

Makes a POST request to the /upload endpoint:
const response = await fetch(`${API_BASE_URL}/upload`, {
  method: 'POST',
  body: formData,
});
Headers: Not manually set (browser automatically sets Content-Type: multipart/form-data with boundary)

3. Response Validation

Checks for HTTP errors:
if (!response.ok) {
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

4. Content Type Check

Validates that the response is a file (not JSON error):
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
  const errorData = await response.json();
  throw new Error(errorData.error || 'Server returned JSON instead of file');
}
This handles cases where the backend returns an error as JSON instead of a file.

5. Blob Return

Converts the response to a Blob:
return response.blob();

Error Handling

The service implements comprehensive error handling:

Connection Errors

if (error instanceof TypeError && error.message.includes('fetch')) {
  throw new Error('Cannot connect to server. Make sure backend is running on ' + API_BASE_URL);
}
Detects network failures and provides a user-friendly message.

HTTP Errors

if (!response.ok) {
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
Throws errors for 4xx and 5xx status codes.

Backend Errors

if (contentType && contentType.includes('application/json')) {
  const errorData = await response.json();
  throw new Error(errorData.error || 'Server returned JSON instead of file');
}
Parses JSON error responses from the backend.

Backend Endpoints

The backend must implement the following endpoints:

POST /upload

Converts uploaded files to Markdown format.

Request

Method: POST
Content-Type: multipart/form-data
Body: FormData with files field containing one or more files
POST /upload HTTP/1.1
Host: localhost:5001
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="files"; filename="document.pdf"
Content-Type: application/pdf

[binary file data]
------WebKitFormBoundary--

Response (Success)

Single file:
HTTP/1.1 200 OK
Content-Type: text/markdown
Content-Disposition: attachment; filename="document.md"

[markdown content]
Multiple files:
HTTP/1.1 200 OK
Content-Type: application/zip
Content-Disposition: attachment; filename="converted_files.zip"

[zip binary data]

Response (Error)

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error": "Invalid file format"
}

POST /md-to-word

Converts Markdown files to Word format (used by PandocConverter component).

Request

POST /md-to-word HTTP/1.1
Host: localhost:5001
Content-Type: multipart/form-data

[FormData with files]

Response

HTTP/1.1 200 OK
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document
Content-Disposition: attachment; filename="document.docx"

[docx binary data]

Usage Example

Here’s how the API service is used in the useFileUpload hook:
import { uploadFiles } from '../services/api';

const handleUpload = async (files: FileList) => {
  setIsUploading(true);
  setProgress(0);

  try {
    // Call API service
    const blob = await uploadFiles(files);
    
    // Download the result
    const fileName = files.length === 1 ? 
      `${files[0].name.split('.')[0]}.md` : 
      'converted_files.zip';
    
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = fileName;
    document.body.appendChild(a);
    a.click();
    window.URL.revokeObjectURL(url);
    document.body.removeChild(a);

    setIsUploading(false);
    setShowSuccess(true);

  } catch (error) {
    setIsUploading(false);
    alert(`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
  }
};

Request/Response Flow

TypeScript Types

The API service uses built-in browser types:
// Input
FileList: DOM interface for file input elements

// Output
Blob: Binary Large Object for file data

// Error
Error: Standard JavaScript error object

Best Practices

The service provides context-specific error messages:
  • Network errors mention the backend URL
  • HTTP errors include status codes
  • Backend errors use the server’s error message
Always check the response Content-Type header to distinguish between:
  • Success responses (file blobs)
  • Error responses (JSON)
Use Vite’s import.meta.env for configuration:
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
All custom env vars must be prefixed with VITE_.
  • Don’t set Content-Type header manually
  • Browser automatically adds boundary parameter
  • Use the same field name for multiple files

Testing the API

Test the backend endpoint using curl:
curl -X POST http://localhost:5001/upload \
  -F "files=@document.pdf" \
  -o output.md
For multiple files:
curl -X POST http://localhost:5001/upload \
  -F "files=@document1.pdf" \
  -F "files=@document2.docx" \
  -o output.zip

Troubleshooting

If you see CORS errors, ensure the backend allows requests from your frontend origin:
# Flask example
from flask_cors import CORS
CORS(app, origins=['http://localhost:5173'])
Error: “Cannot connect to server”Solution: Verify the backend is running:
curl http://localhost:5001/health
Ensure the response includes proper headers:
return send_file(
    file_path,
    as_attachment=True,
    download_name='converted.md'
)

See Also

Build docs developers (and LLMs) love