Skip to content

Commit d00d938

Browse files
committed
feat(send): add download response methods 💾
- Add documentation for download features - Add Send.data() method for in-memory data downloads - Add Send.file() method for filesystem file downloads - Update cross-references in existing documentation - Update navigation menu with new download sections
1 parent 5d2821f commit d00d938

9 files changed

Lines changed: 402 additions & 72 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ HTTP server with file-based routing for Deno that supports middleware and dynami
2020
- [CORS Middleware](https://docs-deserve.neabyte.com/middleware/cors) - Cross-origin request handling
2121

2222
- **Response Utilities**
23+
- [Data Downloads](https://docs-deserve.neabyte.com/response/data) - Download in-memory content
24+
- [File Downloads](https://docs-deserve.neabyte.com/response/file) - Download files from filesystem
2325
- [JSON Format](https://docs-deserve.neabyte.com/response/json) - Create JSON responses easily
2426
- [Text Format](https://docs-deserve.neabyte.com/response/text) - Plain text responses
2527
- [HTML Format](https://docs-deserve.neabyte.com/response/html) - HTML content responses

docs/.vitepress/config.ts

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,54 +9,54 @@ export default defineConfig({
99
themeConfig: {
1010
logo: '/logo.svg',
1111
nav: [{ text: 'Home', link: '/' }],
12-
sidebar: {
13-
'/': [
14-
{
15-
text: 'Getting Started',
16-
items: [
17-
{ text: 'Installation', link: '/getting-started/installation' },
18-
{ text: 'Quick Start', link: '/getting-started/quick-start' },
19-
{ text: 'Custom Configuration', link: '/getting-started/custom-configuration' }
20-
]
21-
},
22-
{
23-
text: 'Core Concepts & Rules',
24-
items: [
25-
{ text: 'File-based Routing', link: '/core-concepts/file-based-routing' },
26-
{ text: 'Route Patterns', link: '/core-concepts/route-patterns' },
27-
{ text: 'HTTP Methods', link: '/core-concepts/http-methods' }
28-
]
29-
},
30-
{
31-
text: 'Middleware',
32-
items: [
33-
{ text: 'Global Middleware', link: '/middleware/global' },
34-
{ text: 'Route-Specific Middleware', link: '/middleware/route-specific' },
35-
{ text: 'CORS Middleware', link: '/middleware/cors' }
36-
]
37-
},
38-
{
39-
text: 'Send Response Utility',
40-
items: [
41-
{ text: 'JSON Format', link: '/response/json' },
42-
{ text: 'Text Format', link: '/response/text' },
43-
{ text: 'HTML Format', link: '/response/html' },
44-
{ text: 'Redirect', link: '/response/redirect' }
45-
]
46-
},
47-
{
48-
text: 'Static File Serving',
49-
items: [
50-
{ text: 'Basic Static Serving', link: '/static-file/basic' },
51-
{ text: 'Multiple Directories', link: '/static-file/multiple' }
52-
]
53-
},
54-
{
55-
text: 'Error Handling',
56-
items: [{ text: 'Object Details', link: '/error-handling/object-details' }]
57-
}
58-
]
59-
},
12+
sidebar: [
13+
{
14+
text: 'Getting Started',
15+
items: [
16+
{ text: 'Installation', link: '/getting-started/installation' },
17+
{ text: 'Quick Start', link: '/getting-started/quick-start' },
18+
{ text: 'Custom Configuration', link: '/getting-started/custom-configuration' }
19+
]
20+
},
21+
{
22+
text: 'Core Concepts',
23+
items: [
24+
{ text: 'File-based Routing', link: '/core-concepts/file-based-routing' },
25+
{ text: 'Route Patterns', link: '/core-concepts/route-patterns' },
26+
{ text: 'HTTP Methods', link: '/core-concepts/http-methods' }
27+
]
28+
},
29+
{
30+
text: 'Middleware',
31+
items: [
32+
{ text: 'Global Middleware', link: '/middleware/global' },
33+
{ text: 'Route-Specific Middleware', link: '/middleware/route-specific' },
34+
{ text: 'CORS Middleware', link: '/middleware/cors' }
35+
]
36+
},
37+
{
38+
text: 'Response Utilities',
39+
items: [
40+
{ text: 'Data Downloads', link: '/response/data' },
41+
{ text: 'File Downloads', link: '/response/file' },
42+
{ text: 'HTML Format', link: '/response/html' },
43+
{ text: 'JSON Format', link: '/response/json' },
44+
{ text: 'Redirect', link: '/response/redirect' },
45+
{ text: 'Text Format', link: '/response/text' }
46+
]
47+
},
48+
{
49+
text: 'Static Files',
50+
items: [
51+
{ text: 'Basic Static Serving', link: '/static-file/basic' },
52+
{ text: 'Multiple Directories', link: '/static-file/multiple' }
53+
]
54+
},
55+
{
56+
text: 'Error Handling',
57+
items: [{ text: 'Object Details', link: '/error-handling/object-details' }]
58+
}
59+
],
6060
socialLinks: [{ icon: 'github', link: 'https://github.com/NeaByteLab/Deserve' }],
6161
search: {
6262
provider: 'local'

docs/response/data.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Data Downloads
2+
3+
The `Send.data()` method creates download responses from in-memory data (strings or binary data) with proper headers and filename handling.
4+
5+
## Basic Usage
6+
7+
```typescript
8+
import { Send } from '@neabyte/deserve'
9+
10+
export function GET(req: Request): Response {
11+
const csvData = 'name,email\nJohn,john@example.com\nJane,jane@example.com'
12+
return Send.data(csvData, 'users.csv', undefined, 'text/csv')
13+
}
14+
```
15+
16+
## String Data
17+
18+
```typescript
19+
export function POST(req: Request): Response {
20+
const jsonData = JSON.stringify({ users: ['alice', 'bob'] }, null, 2)
21+
return Send.data(jsonData, 'users.json', undefined, 'application/json')
22+
}
23+
```
24+
25+
## Binary Data
26+
27+
```typescript
28+
export function GET(req: Request): Response {
29+
const binaryData = new Uint8Array([1, 2, 3, 4, 5])
30+
return Send.data(binaryData, 'data.bin')
31+
}
32+
```
33+
34+
## With Custom Headers
35+
36+
```typescript
37+
export function GET(req: Request): Response {
38+
const content = 'Hello World!'
39+
return Send.data(
40+
content,
41+
'hello.txt',
42+
{
43+
headers: {
44+
'Cache-Control': 'no-cache',
45+
'X-Custom-Header': 'value'
46+
}
47+
},
48+
'text/plain'
49+
)
50+
}
51+
```
52+
53+
## CSV Export Example
54+
55+
```typescript
56+
export function GET(req: Request): Response {
57+
const users = [
58+
{ id: 1, name: 'John Doe', email: 'john@example.com' },
59+
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
60+
]
61+
const csvData = 'id,name,email\n' +
62+
users.map(user => `${user.id},${user.name},${user.email}`).join('\n')
63+
return Send.data(csvData, 'users.csv', undefined, 'text/csv')
64+
}
65+
```
66+
67+
## JSON Export Example
68+
69+
```typescript
70+
export function GET(req: Request): Response {
71+
const data = {
72+
users: [
73+
{ id: 1, name: 'John Doe', email: 'john@example.com' },
74+
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
75+
],
76+
timestamp: new Date().toISOString()
77+
}
78+
return Send.data(
79+
JSON.stringify(data, null, 2),
80+
'export.json',
81+
undefined,
82+
'application/json'
83+
)
84+
}
85+
```
86+
87+
## When to Use Data Downloads
88+
89+
- **Generated content** - CSV exports, reports, logs
90+
- **API responses** - When you want to force download instead of display
91+
- **Binary data** - Images, documents, archives
92+
- **Dynamic files** - Content generated from database queries
93+
- **Export functionality** - User data exports, backup files
94+
95+
## Parameters
96+
97+
- `data` - The content to download (string or Uint8Array)
98+
- `filename` - The filename for the download
99+
- `options` - Additional ResponseInit options (optional)
100+
- `contentType` - MIME type (default: 'application/octet-stream')
101+
102+
## Next Steps
103+
104+
- [File Downloads](/response/file) - Download files from filesystem
105+
- [HTML Responses](/response/html) - Rich HTML content
106+
- [Redirects](/response/redirect) - Redirect responses

docs/response/file.md

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# File Downloads
2+
3+
The `Send.file()` method creates download responses from files on the filesystem.
4+
5+
## Basic Usage
6+
7+
```typescript
8+
import { Send } from '@neabyte/deserve'
9+
10+
export async function GET(req: Request): Response {
11+
return await Send.file('./uploads/document.pdf', 'monthly-report.pdf')
12+
}
13+
```
14+
15+
## With Absolute Paths
16+
17+
```typescript
18+
export async function GET(req: Request): Response {
19+
return await Send.file(`${Deno.cwd()}/downloads/sample.txt`, 'hello-world.txt')
20+
}
21+
```
22+
23+
## Auto-Generated Filename
24+
25+
```typescript
26+
export async function GET(req: Request): Response {
27+
// Filename will be extracted from the file path
28+
return await Send.file('./uploads/report.pdf')
29+
}
30+
```
31+
32+
## With Custom Headers
33+
34+
```typescript
35+
export async function GET(req: Request): Response {
36+
return await Send.file(
37+
'./uploads/document.pdf',
38+
'custom-name.pdf',
39+
{
40+
headers: {
41+
'Cache-Control': 'public, max-age=3600',
42+
'X-Custom-Header': 'value'
43+
}
44+
}
45+
)
46+
}
47+
```
48+
49+
## Dynamic File Downloads
50+
51+
```typescript
52+
// routes/downloads/[filename].ts
53+
export async function GET(req: Request, params: Record<string, string>): Response {
54+
const { filename } = params
55+
// Validate filename to prevent directory traversal
56+
if (filename.includes('..') || filename.includes('/')) {
57+
return Send.json({ error: 'Invalid filename' }, { status: 400 })
58+
}
59+
try {
60+
return await Send.file(`${Deno.cwd()}/downloads/${filename}`)
61+
} catch (error) {
62+
return Send.json({ error: 'File not found' }, { status: 404 })
63+
}
64+
}
65+
```
66+
67+
## File Type Detection
68+
69+
```typescript
70+
export async function GET(req: Request): Response {
71+
const url = new URL(req.url)
72+
const fileType = url.searchParams.get('type') || 'pdf'
73+
const fileMap = {
74+
pdf: './uploads/document.pdf',
75+
doc: './uploads/document.doc',
76+
txt: './uploads/document.txt'
77+
}
78+
const filePath = fileMap[fileType as keyof typeof fileMap]
79+
if (!filePath) {
80+
return Send.json({ error: 'Unsupported file type' }, { status: 400 })
81+
}
82+
return await Send.file(filePath, `document.${fileType}`)
83+
}
84+
```
85+
86+
## Error Handling
87+
88+
```typescript
89+
export async function GET(req: Request): Response {
90+
try {
91+
return await Send.file('./uploads/document.pdf', 'report.pdf')
92+
} catch (error) {
93+
return Send.json(
94+
{ error: 'File not found or cannot be read' },
95+
{ status: 404 }
96+
)
97+
}
98+
}
99+
```
100+
101+
## Multiple File Types
102+
103+
```typescript
104+
export async function GET(req: Request): Response {
105+
const url = new URL(req.url)
106+
const format = url.searchParams.get('format') || 'pdf'
107+
const files = {
108+
pdf: './reports/monthly-report.pdf',
109+
excel: './reports/monthly-report.xlsx',
110+
csv: './reports/monthly-report.csv'
111+
}
112+
const filePath = files[format as keyof typeof files]
113+
if (!filePath) {
114+
return Send.json({ error: 'Unsupported format' }, { status: 400 })
115+
}
116+
return await Send.file(filePath, `monthly-report.${format}`)
117+
}
118+
```
119+
120+
## When to Use File Downloads
121+
122+
- **Static files** - Documents, images, archives
123+
- **User uploads** - Files uploaded by users
124+
- **Generated reports** - PDF reports, Excel files
125+
- **Media files** - Images, videos, audio
126+
- **Backup files** - Database dumps, configuration files
127+
128+
## Parameters
129+
130+
- `filePath` - Path to the file on the filesystem
131+
- `filename` - Optional custom filename for download (defaults to file basename)
132+
- `options` - Additional ResponseInit options (optional)
133+
134+
## Security Considerations
135+
136+
- **Validate file paths** - Prevent directory traversal attacks
137+
- **Check file permissions** - Ensure files are readable
138+
- **Sanitize filenames** - Remove dangerous characters
139+
- **Use absolute paths** - Avoid relative path confusion
140+
141+
## Example Security Implementation
142+
143+
```typescript
144+
export async function GET(req: Request, params: Record<string, string>): Response {
145+
const { filename } = params
146+
// Security checks
147+
if (!filename || filename.includes('..') || filename.includes('/')) {
148+
return Send.json({ error: 'Invalid filename' }, { status: 400 })
149+
}
150+
// Only allow specific file extensions
151+
const allowedExtensions = ['.pdf', '.txt', '.jpg', '.png']
152+
const hasValidExtension = allowedExtensions.some(ext => filename.endsWith(ext))
153+
if (!hasValidExtension) {
154+
return Send.json({ error: 'File type not allowed' }, { status: 400 })
155+
}
156+
try {
157+
return await Send.file(`${Deno.cwd()}/downloads/${filename}`)
158+
} catch (error) {
159+
return Send.json({ error: 'File not found' }, { status: 404 })
160+
}
161+
}
162+
```
163+
164+
## Next Steps
165+
166+
- [JSON Responses](/response/json) - Structured data responses
167+
- [Text Responses](/response/text) - Plain text responses
168+
- [Static File Serving](/static-file/basic) - Serve static files

docs/response/html.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,6 @@ export function GET(req: Request): Response {
105105

106106
## Next Steps
107107

108-
- [Redirects](/response/redirect) - Redirect responses
109-
- [Text Responses](/response/text) - Plain text
110-
- [JSON Responses](/response/json) - Structured data
108+
- [Data Downloads](/response/data) - Download generated content
109+
- [File Downloads](/response/file) - Download files from filesystem
110+
- [Error Handling](/error-handling/object-details) - Handle errors properly

0 commit comments

Comments
 (0)