Axios Advanced — Uploads, Caching, Testing & Production Patterns
Axios goes beyond simple API calls — it handles file uploads with progress tracking, concurrent request management, caching, testing with mocks, and production-ready configuration for security and reliability.
What You’ll Learn
By the end of this tutorial, you’ll upload single and multiple files with real-time progress bars, download files as blobs with download progress, manage concurrent requests with Promise.all, implement caching strategies (in-memory, ETag, third-party), test Axios code with msw and axios-mock-adapter, and configure a production-ready Axios instance with CSRF protection, retries, and environment-specific settings.
Why Advanced Patterns Matter
The basic Axios examples you’ve seen so far work for simple apps, but real applications need more:
- File uploads — users need to upload profile pictures, PDFs, or CSV files, and they want to see progress
- Concurrent requests — a dashboard needs to load users, posts, and comments simultaneously, not one after another
- Cancellation — when the user types in a search box, you need to cancel the previous request to avoid race conditions
- Caching — some data doesn’t change often; fetching it every time wastes bandwidth and slows the app
- Testing — you can’t rely on a real server in tests; you need to mock responses
- Production hardening — timeouts, retries, CSRF protection, and environment-specific config prevent real-world failures
Real-world use: Durga Antivirus Pro uses advanced Axios patterns for uploading suspicious files to its cloud scanning service — with per-file progress bars, automatic retry on network failure, cancellation when the user navigates away, and ETag-based caching for threat signature databases that update hourly.
Where This Fits in Your Learning Path
flowchart LR
A["Axios Getting Started"] --> B["Requests & Config"]
B --> C["Interceptors & Error Handling"]
C --> D["**Axios Advanced**"]
D --> E["Full-Stack API Apps"]
style D fill:#f97316,stroke:#c2410c,color:#fff
style A fill:#e5e7eb,stroke:#9ca3af,color:#374151
style E fill:#22c55e,stroke:#16a34a,color:#22c55e
File Upload with Progress Tracking
Uploading files is different from sending JSON. You need to use FormData and set the content type to multipart/form-data.
Single File Upload with Progress Bar
async function uploadFile(file, onProgress) {
const formData = new FormData()
formData.append('file', file) // The actual file
formData.append('description', 'Uploaded file')
const { data } = await axios.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
// Calculate percentage: (bytes sent / total bytes) * 100
const percent = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
)
onProgress?.(percent)
},
})
return data
}
// Usage
const file = event.target.files[0]
uploadFile(file, (percent) => {
console.log(`${percent}% uploaded`)
// Update a progress bar: progressBar.style.width = percent + '%'
})What’s happening step by step:
FormDatawraps the file into a format suitable for multipart uploadsaxios.postsends it withContent-Type: multipart/form-dataonUploadProgressis called by the browser as chunks are sentprogressEvent.loaded= bytes sent so far,progressEvent.total= total bytes- The callback updates the UI with the percentage
Uploading Multiple Files
async function uploadMultiple(files, onProgress) {
const formData = new FormData()
files.forEach((file) => formData.append('files', file))
const { data } = await axios.post('/upload-multiple', formData, {
onUploadProgress: (e) => {
const percent = Math.round((e.loaded * 100) / e.total)
onProgress?.(percent)
},
})
return data
}Important: When uploading multiple files in one request, the progress shows the combined upload of all files, not per-file progress. For per-file progress bars, upload each file in a separate request.
File Download with Blob
Downloading files requires telling Axios to expect binary data (responseType: 'blob').
async function downloadFile(url, filename) {
const response = await axios.get(url, {
responseType: 'blob',
onDownloadProgress: (progressEvent) => {
const percent = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
)
console.log(`${percent}% downloaded`)
},
})
// Create a temporary URL for the blob
const blob = new Blob([response.data])
const blobUrl = window.URL.createObjectURL(blob)
// Trigger the browser's download dialog
const link = document.createElement('a')
link.href = blobUrl
link.download = filename || 'download'
document.body.appendChild(link)
link.click()
// Clean up
document.body.removeChild(link)
window.URL.revokeObjectURL(blobUrl)
}
// Usage
downloadFile('https://example.com/file.pdf', 'document.pdf')Why responseType: 'blob'? Without it, Axios tries to parse the response as text. Binary data (PDFs, images, ZIPs) gets garbled into nonsense characters.
Why URL.revokeObjectURL? Blob URLs consume memory until the page is closed. Revoking them immediately after download prevents memory leaks.
Concurrent Requests
When your page needs data from multiple endpoints, don’t fetch them one after another — that wastes time. Fetch them simultaneously.
Sequential (Slow)
const users = await axios.get('/api/users') // Wait 200ms
const posts = await axios.get('/api/posts') // Wait 200ms
const comments = await axios.get('/api/comments') // Wait 200ms
// Total: 600ms
Concurrent (Fast)
async function fetchAll() {
const [users, posts, comments] = await Promise.all([
axios.get('/api/users'),
axios.get('/api/posts'),
axios.get('/api/comments'),
])
return {
users: users.data,
posts: posts.data,
comments: comments.data,
}
}
// Total: ~200ms (all three run in parallel)
Promise.all sends all three requests at the same time and waits for all of them to complete. The total time is the time of the slowest request, not the sum.
Conditional Concurrency
Sometimes you don’t know in advance how many requests you’ll need:
async function fetchConditional(userId) {
const requests = [axios.get(`/api/users/${userId}`)]
// Only fetch posts if we have a valid user
if (userId > 0) {
requests.push(axios.get(`/api/users/${userId}/posts`))
}
const [user, posts] = await Promise.all(requests)
return { user: user.data, posts: posts?.data }
}Request Cancellation in Practice
You’ve seen cancellation basics. Here’s how it’s used in real scenarios.
Search Autocomplete
The classic pattern: every keystroke triggers a search request. If the user types faster than the API responds, cancel the previous request:
let controller
async function search(query) {
// Cancel the previous in-flight request
if (controller) controller.abort()
// Create a new controller for this request
controller = new AbortController()
try {
const { data } = await axios.get('/api/search', {
params: { q: query },
signal: controller.signal,
})
return data
} catch (error) {
// Ignore cancellation errors — they're expected
if (!axios.isCancel(error)) throw error
}
}Why this matters: Without cancellation, if the user types “jav” then “javascript” quickly, both requests go out. If the “jav” response arrives after “javascript”, the results flicker and may show wrong data.
Cancelling on Page Navigation
const controller = new AbortController()
// Fetch data for the current page
axios.get('/api/dashboard-data', { signal: controller.signal })
// Cleanup when user leaves
window.addEventListener('beforeunload', () => {
controller.abort()
})Caching Strategies
Caching prevents fetching the same data twice, improving performance and reducing server load.
Simple In-Memory Cache
const cache = new Map()
async function fetchWithCache(url, ttl = 60000) { // 60 second TTL
const cached = cache.get(url)
if (cached && Date.now() - cached.timestamp < ttl) {
console.log('Cache hit:', url)
return cached.data
}
console.log('Cache miss — fetching:', url)
const { data } = await axios.get(url)
cache.set(url, { data, timestamp: Date.now() })
return data
}ETag Caching
ETags are headers the server sends with responses. The browser sends the ETag back on subsequent requests, and the server replies with 304 Not Modified if nothing changed.
let etag = null
let cachedData = null
async function fetchWithEtag(url) {
const headers = {}
if (etag) headers['If-None-Match'] = etag // Send stored ETag
try {
const response = await axios.get(url, { headers })
if (response.status === 304) {
// Server says "not modified" — use cached data
console.log('Using cached data (304)')
return cachedData
}
// Store the new ETag and data
etag = response.headers.etag
cachedData = response.data
return response.data
} catch (error) {
// On error, return cached data if available
return cachedData || null
}
}Third-Party: axios-cache-interceptor
npm install axios-cache-interceptorimport axios from 'axios'
import { setupCache } from 'axios-cache-interceptor'
const api = setupCache(axios.create({
baseURL: '/api',
timeout: 5000,
}))
// Automatically cached
const users = await api.get('/users')
// Force fresh request
const fresh = await api.get('/users', { cache: false })Testing with Mocks
Never make real HTTP requests in tests. Mock the responses instead.
Using axios-mock-adapter
npm install axios-mock-adapterimport axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
const mock = new MockAdapter(axios)
// Mock GET /api/users
mock.onGet('/api/users').reply(200, [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
])
// Mock POST with specific body
mock.onPost('/api/users', { name: 'Charlie' }).reply(201, {
id: 3,
name: 'Charlie',
})
// Mock errors
mock.onGet('/api/error').networkError()
mock.onGet('/api/timeout').timeout()
// Now test
const response = await axios.get('/api/users')
console.log(response.data) // [{ id: 1, name: 'Alice' }, ...]
Using msw (Mock Service Worker)
npm install msw --save-devimport { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
const server = setupServer(
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
])
})
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
test('fetches users', async () => {
const { data } = await axios.get('/api/users')
expect(data).toHaveLength(2)
})msw intercepts at the network level, so it works with any HTTP client — not just Axios. This is the recommended approach for modern apps.
Production-Ready Configuration
A production Axios instance should handle timeouts, retries, auth tokens, CSRF protection, and error logging.
import axios from 'axios'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
withCredentials: true, // Send cookies cross-origin
})
// Request interceptor: auth token + request tracking
api.interceptors.request.use((config) => {
const token = localStorage.getItem('authToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
config.headers['X-Request-Id'] = crypto.randomUUID()
return config
})
// Response interceptor: error handling + token refresh
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Attempt token refresh
try {
const { data } = await axios.post('/auth/refresh', {
refreshToken: localStorage.getItem('refreshToken'),
})
localStorage.setItem('authToken', data.accessToken)
error.config.headers.Authorization = `Bearer ${data.accessToken}`
return api(error.config)
} catch {
localStorage.clear()
window.location.href = '/login'
}
}
// Log to monitoring service
console.error('[API Error]', {
url: error.config?.url,
status: error.response?.status,
message: error.message,
})
return Promise.reject(error)
}
)
export default apiCSRF Protection
Axios has built-in CSRF protection. It reads a token from a cookie and sends it as a header:
const api = axios.create({
baseURL: '/api',
xsrfCookieName: 'XSRF-TOKEN', // Cookie name
xsrfHeaderName: 'X-XSRF-TOKEN', // Header name
withCredentials: true,
})
// Axios automatically reads XSRF-TOKEN cookie and sets X-XSRF-TOKEN header
Environment-Specific Configuration
const config = {
development: {
baseURL: 'http://localhost:3000/api',
timeout: 15000,
},
production: {
baseURL: 'https://api.example.com',
timeout: 10000,
},
test: {
baseURL: 'https://test-api.example.com',
timeout: 5000,
},
}
const env = import.meta.env.MODE || 'development'
const api = axios.create(config[env])Common Mistakes Beginners Make
1. Not Setting responseType: 'blob' for Downloads
// Wrong: garbled binary data
const res = await axios.get('/file.pdf')
// Correct: Axios treats response as binary
const res = await axios.get('/file.pdf', { responseType: 'blob' })2. Cancelling the Wrong Controller in Search
// Wrong: creates new controller but doesn't track the old one
function search(query) {
const controller = new AbortController() // New controller every call
// ...
}
// Correct: store and abort the previous controller
let currentController
function search(query) {
currentController?.abort()
currentController = new AbortController()
return axios.get('/search', { signal: currentController.signal })
}3. Forgetting to Revoke Blob URLs
// Wrong: memory leak! The blob URL never gets freed
const url = URL.createObjectURL(blob)
link.href = url
// Correct: revoke after download completes
link.onclick = () => setTimeout(() => URL.revokeObjectURL(url), 10000)4. Uploading Files Without FormData
// Wrong: sending raw file object as JSON
axios.post('/upload', file)
// Correct: wrap in FormData
const formData = new FormData()
formData.append('file', file)
axios.post('/upload', formData)5. Fetching Sequentially When Concurrent Would Work
// Wrong: 3 sequential requests = 600ms
const users = await axios.get('/api/users')
const posts = await axios.get('/api/posts')
const comments = await axios.get('/api/comments')
// Correct: 3 concurrent requests = ~200ms
const [users, posts, comments] = await Promise.all([
axios.get('/api/users'),
axios.get('/api/posts'),
axios.get('/api/comments'),
])Practice Questions
Why must you set
responseType: 'blob'when downloading a file? Without it, Axios parses the response as text, corrupting binary data like PDFs or images.How does
Promise.allimprove performance over sequentialawaitcalls? It sends all requests simultaneously. The total time equals the slowest request, not the sum of all requests.Why do you need to track the active
AbortControllerin a search autocomplete? To cancel the previous request when the user types a new character, preventing race conditions where old results overwrite new ones.What is the difference between
axios-mock-adapterandmsw?axios-mock-adapterintercepts Axios-specific requests.mswintercepts at the network level and works with any HTTP client.What does
URL.revokeObjectURLdo and why is it important? It frees memory associated with a blob URL. Without it, blob URLs accumulate until the page is closed, causing memory leaks.
Challenge
Build a file management dashboard that:
- Accepts drag-and-drop file uploads with per-file progress bars
- Uploads files concurrently (max 3 at a time)
- Allows cancelling individual uploads or all at once
- Shows upload speed (bytes/second) for each file
- Lets users preview uploaded images in a new tab
- Displays stats: total files, completed, failed, and average speed
FAQ
Try It Yourself: File Upload Dashboard
This interactive dashboard demonstrates all the advanced patterns you’ve learned — drag-and-drop upload, per-file progress, cancellation, and stats.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Axios File Upload Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/axios@1.7.0/dist/axios.min.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
.container { max-width: 1000px; margin: 0 auto; }
h1 { font-size: 1.75rem; }
.subtitle { color: #94a3b8; margin-bottom: 1.5rem; }
.card { background: #1e293b; border-radius: 0.75rem; padding: 1.5rem; border: 1px solid #334155; margin-bottom: 1rem; }
.card h2 { font-size: 1.05rem; margin-bottom: 0.75rem; color: #38bdf8; }
.dropzone { border: 2px dashed #334155; border-radius: 0.75rem; padding: 3rem 2rem; text-align: center; cursor: pointer; transition: all 0.2s; }
.dropzone:hover, .dropzone.dragover { border-color: #38bdf8; background: rgba(56, 189, 248, 0.05); }
.dropzone.dragover { border-color: #22c55e; background: rgba(34, 197, 94, 0.05); }
.dropzone p { color: #94a3b8; font-size: 0.95rem; }
.dropzone .icon { font-size: 2.5rem; margin-bottom: 0.5rem; }
.flex { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.btn { background: #38bdf8; color: #0f172a; border: none; padding: 0.5rem 1.25rem; border-radius: 0.375rem; font-weight: 600; cursor: pointer; font-size: 0.875rem; }
.btn:hover { background: #7dd3fc; }
.btn-outline { background: transparent; color: #38bdf8; border: 1px solid #38bdf8; }
.btn-outline:hover { background: #38bdf8; color: #0f172a; }
.btn-danger { background: #ef4444; color: #fff; }
.btn-danger:hover { background: #dc2626; }
.btn-sm { padding: 0.25rem 0.5rem; font-size: 0.75rem; }
.btn-success { background: #22c55e; color: #0f172a; }
.file-item { background: #0f172a; border: 1px solid #334155; border-radius: 0.5rem; padding: 0.75rem; margin-bottom: 0.5rem; }
.file-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.375rem; }
.file-name { font-weight: 600; font-size: 0.9rem; }
.file-size { color: #94a3b8; font-size: 0.75rem; }
.file-status { font-size: 0.75rem; padding: 0.125rem 0.375rem; border-radius: 0.25rem; }
.file-status.pending { background: #334155; color: #94a3b8; }
.file-status.uploading { background: #f59e0b; color: #0f172a; }
.file-status.done { background: #22c55e; color: #0f172a; }
.file-status.error { background: #ef4444; color: #fff; }
.file-status.cancelled { background: #475569; color: #94a3b8; }
.progress-bar { background: #1e293b; border-radius: 999px; height: 6px; margin-top: 0.375rem; overflow: hidden; }
.progress-fill { height: 100%; border-radius: 999px; transition: width 0.2s; width: 0%; }
.progress-fill.uploading { background: linear-gradient(90deg, #38bdf8, #22c55e); }
.progress-fill.done { background: #22c55e; }
.progress-fill.error { background: #ef4444; }
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.75rem; margin-top: 1rem; }
.stat-card { background: #0f172a; padding: 0.75rem; border-radius: 0.5rem; text-align: center; }
.stat-value { font-size: 1.5rem; font-weight: 700; color: #38bdf8; }
.stat-label { font-size: 0.75rem; color: #94a3b8; }
.file-list { max-height: 400px; overflow-y: auto; }
.empty-state { color: #475569; text-align: center; padding: 2rem; font-size: 0.9rem; }
@media (max-width: 600px) { .stats { grid-template-columns: repeat(2, 1fr); } }
</style>
</head>
<body>
<div class="container">
<h1>File Upload Dashboard</h1>
<p class="subtitle">Drag and drop files, track progress, cancel uploads</p>
<div class="card">
<div class="dropzone" id="dropzone">
<div class="icon">📦</div>
<p>Drag and drop files here, or <strong style="color:#38bdf8;">click to browse</strong></p>
<p style="font-size:0.8rem;margin-top:0.5rem;">Any file type accepted</p>
</div>
<input type="file" id="fileInput" multiple style="display:none;" />
<div class="flex" style="margin-top:1rem;">
<button class="btn btn-success btn-sm" onclick="uploadAll()">Upload All</button>
<button class="btn btn-outline btn-sm" onclick="cancelAll()">Cancel All</button>
<button class="btn btn-danger btn-sm" onclick="clearCompleted()">Clear Completed</button>
</div>
</div>
<div class="card">
<h2>Files</h2>
<div class="file-list" id="fileList">
<div class="empty-state">No files added yet. Drop some above.</div>
</div>
<div class="stats">
<div class="stat-card"><div class="stat-value" id="totalStat">0</div><div class="stat-label">Total</div></div>
<div class="stat-card"><div class="stat-value" id="uploadedStat">0</div><div class="stat-label">Uploaded</div></div>
<div class="stat-card"><div class="stat-value" id="failedStat">0</div><div class="stat-label">Failed</div></div>
<div class="stat-card"><div class="stat-value" id="speedStat">0 B/s</div><div class="stat-label">Speed</div></div>
</div>
</div>
</div>
<script>
const files = new Map();
let fileIdCounter = 0;
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
dropzone.addEventListener('click', () => fileInput.click());
dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.classList.add('dragover'); });
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dragover'));
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('dragover');
handleFiles(Array.from(e.dataTransfer.files));
});
fileInput.addEventListener('change', () => {
handleFiles(Array.from(fileInput.files));
fileInput.value = '';
});
function handleFiles(newFiles) {
newFiles.forEach(file => {
const id = ++fileIdCounter;
files.set(id, { id, file, name: file.name, size: file.size, status: 'pending', progress: 0, controller: null, speed: 0 });
});
renderFileList();
updateStats();
}
function renderFileList() {
const container = document.getElementById('fileList');
if (files.size === 0) {
container.innerHTML = '<div class="empty-state">No files added yet. Drop some above.</div>';
return;
}
container.innerHTML = Array.from(files.values()).map(f => {
const sizeStr = f.size > 1024 * 1024 ? (f.size / (1024 * 1024)).toFixed(1) + ' MB' : (f.size / 1024).toFixed(1) + ' KB';
const statusClass = f.status === 'uploading' ? 'uploading' : f.status;
return `
<div class="file-item">
<div class="file-header">
<div><span class="file-name">${f.name}</span> <span class="file-size">${sizeStr}</span></div>
<div class="flex">
<span class="file-status ${statusClass}">${f.status}</span>
${f.status === 'pending' ? `<button class="btn btn-sm btn-success" onclick="uploadFile(${f.id})">Upload</button>` : ''}
${f.status === 'uploading' ? `<button class="btn btn-sm btn-danger" onclick="cancelFile(${f.id})">Cancel</button>` : ''}
${f.status === 'done' ? `<button class="btn btn-sm btn-outline" onclick="previewFile(${f.id})">Preview</button>` : ''}
${['done', 'error', 'cancelled'].includes(f.status) ? `<button class="btn btn-sm btn-outline" onclick="removeFile(${f.id})">Remove</button>` : ''}
</div>
</div>
<div class="progress-bar"><div class="progress-fill ${statusClass}" style="width:${f.status === 'done' ? 100 : f.progress}%"></div></div>
${f.status === 'uploading' ? `<div style="display:flex;justify-content:space-between;font-size:0.7rem;color:#94a3b8;margin-top:0.25rem;"><span>${f.progress}%</span><span>${f.speed || 0} B/s</span></div>` : ''}
</div>
`;
}).join('');
}
function uploadAll() { files.forEach((f, id) => { if (f.status === 'pending') uploadFile(id); }); }
function cancelAll() { files.forEach((f, id) => { if (f.status === 'uploading') cancelFile(id); }); }
function clearCompleted() { files.forEach((f, id) => { if (['done', 'error', 'cancelled'].includes(f.status)) files.delete(id); }); renderFileList(); updateStats(); }
async function uploadFile(id) {
const f = files.get(id);
if (!f || f.status !== 'pending') return;
f.status = 'uploading';
f.progress = 0;
f.controller = new AbortController();
renderFileList();
const formData = new FormData();
formData.append('file', f.file);
let lastLoaded = 0, lastTime = Date.now();
try {
const response = await axios.post('https://httpbin.org/post', formData, {
signal: f.controller.signal,
onUploadProgress: (e) => {
f.progress = Math.round((e.loaded * 100) / e.total);
const now = Date.now();
const elapsed = (now - lastTime) / 1000;
if (elapsed > 0) f.speed = Math.round((e.loaded - lastLoaded) / elapsed);
lastLoaded = e.loaded; lastTime = now;
renderFileList();
},
});
f.status = 'done'; f.progress = 100; f.responseData = response.data;
} catch (err) {
f.status = axios.isCancel(err) ? 'cancelled' : 'error';
if (!axios.isCancel(err)) f.errorMessage = err.message;
}
renderFileList(); updateStats();
}
function cancelFile(id) { const f = files.get(id); if (f && f.controller) { f.controller.abort(); f.status = 'cancelled'; renderFileList(); updateStats(); } }
function removeFile(id) { files.delete(id); renderFileList(); updateStats(); }
function previewFile(id) { const f = files.get(id); if (!f || !f.responseData) return; const url = URL.createObjectURL(new Blob([f.file])); window.open(url, '_blank'); setTimeout(() => URL.revokeObjectURL(url), 60000); }
function updateStats() { let total = files.size, uploaded = 0, failed = 0; files.forEach(f => { if (f.status === 'done') uploaded++; if (f.status === 'error') failed++; }); document.getElementById('totalStat').textContent = total; document.getElementById('uploadedStat').textContent = uploaded; document.getElementById('failedStat').textContent = failed; }
</script>
</body>
</html>Key Features
- Drag and drop — intuitive file selection via drag/drop or click-to-browse
- Per-file progress bars — visual progress with percentage and speed
- Concurrent uploads — upload all files simultaneously with individual cancellation
- Cancel individual or all — cancel any in-flight upload with
AbortController - Download preview — open completed files in a new tab via blob URL
- Stats dashboard — total / uploaded / failed counts
What’s Next
You’ve completed the Axios series. Next, explore other frontend libraries:
| Library | What You’ll Learn |
|---|---|
| Lodash Getting Started | Utility functions for arrays, objects, and data manipulation |
| Chart.js | Create interactive charts and graphs |
Related topics: JavaScript Promises, File API, REST APIs.
What’s Next
Congratulations on completing this Axios Advanced tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro