gw-file
v0.1.6
Published
Typed browser and server file-upload primitives for S3-compatible object storage.
Maintainers
Readme
gw-file
Typed browser and server primitives for uploading files to S3-compatible object storage. The package keeps application-specific persistence behind a FileRepository interface and returns gw-result values instead of throwing for expected infrastructure failures.
Features
- Two-step browser uploads using presigned S3
PUTURLs - Direct server uploads and object lookup/deletion helpers
- Application-owned file metadata through
FileRepository - Browser image/video metadata extraction
- CDN URL and responsive
srcSethelpers - Separate root, browser, and server entry points
- ESM, CommonJS, and TypeScript declaration output
Requirements
- Node.js 20 or newer for server APIs and package builds
- An S3 bucket and AWS credentials with the operations your application uses
- React 19 when using
gw-file/clientresponsive image components - A Fetch API-compatible runtime for the supplied HTTP handlers
Install
npm install gw-fileInstall the React peers when using responsive image components:
npm install react react-domEntry points
| Import | Purpose | Runtime |
| --- | --- | --- |
| gw-file | CDN URL helpers | Universal |
| gw-file/client | Browser uploader, media metadata, responsive images | Browser |
| gw-file/server | S3 adapter, file service, repository contract, handlers | Node.js |
Do not import gw-file/server from browser bundles. It depends on the AWS SDK and server credentials.
Upload flow
Browser Application API S3
| POST file metadata -> | |
| create DB reference |
| create presigned URL |
| <- file + signedUrl | |
| PUT Blob -------------------------------------------------> |
| <- success ------------------------------------------------ |The application endpoint creates the file reference before the browser uploads the bytes. If the S3 PUT fails, the reference can remain in the repository; applications should clean up abandoned records when needed.
Server setup
1. Configure object storage
Pass the region explicitly for predictable deployments:
import { ObjectStorage } from "gw-file/server";
export const objectStorage = new ObjectStorage({
bucketName: process.env.AWS_S3_BUCKET_NAME!,
region: process.env.AWS_REGION!,
});ObjectStorage passes all remaining options directly to the AWS S3Client. If region is omitted, the AWS SDK resolves it through its standard provider chain, such as AWS_REGION or the shared AWS config file. gw-file does not choose a default region.
AWS credentials use the same provider chain. In deployed AWS environments, prefer an IAM role over long-lived access keys.
For another S3-compatible service, pass its standard client options:
const objectStorage = new ObjectStorage({
bucketName: "uploads",
region: "auto",
endpoint: "https://object-storage.example.com",
forcePathStyle: true,
credentials: {
accessKeyId: process.env.OBJECT_STORAGE_ACCESS_KEY_ID!,
secretAccessKey: process.env.OBJECT_STORAGE_SECRET_ACCESS_KEY!,
},
});2. Implement the repository
The package does not prescribe a database or ORM:
import type { FileRepository } from "gw-file/server";
type AppFile = {
id: string;
userId?: string;
key: string;
name: string;
type?: string;
size?: number;
metadata?: Record<string, unknown>;
};
export const fileRepository: FileRepository<AppFile> = {
async findFileById(fileId) {
return database.files.find(fileId);
},
isForbidden(file, userId) {
return Boolean(file.userId && file.userId !== userId);
},
async createFile(file) {
return database.files.create(file);
},
async deleteFile(fileId) {
await database.files.delete(fileId);
},
};3. Create the service
import { FileService } from "gw-file/server";
export const fileService = new FileService({
prefix: "user",
fileRepository,
objectStorage,
});Generated keys use this shape:
{prefix}/{uuid}/{filename}4. Expose application routes
The supplied handlers use standard Request and Response objects:
import { deleteFileHandler, uploadFileHandler } from "gw-file/server";
export async function POST(request: Request) {
const userId = await getOptionalUserId(request);
return uploadFileHandler({ fileService })(request, { userId });
}
export async function DELETE(
_request: Request,
context: { params: Promise<{ fileId: string }> },
) {
const { fileId } = await context.params;
const userId = await getOptionalUserId(_request);
return deleteFileHandler({ fileRepository })({ userId, fileId })();
}deleteFileHandler deletes only the repository record. If your product must also delete S3 data, look up the file key and call objectStorage.delete(key) as part of your application-owned deletion workflow.
Browser uploads
import { FileUploader } from "gw-file/client";
type AppFile = {
id: string;
key: string;
name: string;
type?: string;
size?: number;
};
const uploader = new FileUploader<AppFile>("/api/files");
const result = await uploader.uploadFile(file, {
metadata: { purpose: "attachment" },
convertToWebp: true,
});
if (result.isErr) {
console.error(result.error);
} else {
console.log(result.value);
}convertToWebp uses browser image and canvas APIs. It is not available in a server-only runtime.
FileUploader returns the gw-result value produced by each request. With gw-result 0.3, failed HTTP responses—including S3 XML errors—are returned as HttpException values instead of being replaced by a generic upload error.
Media metadata
generateMetadata(blob) extracts { width, height } from images and { width, height, poster } from videos. Other file types resolve to an empty object.
Pass uploadBlob when generated video posters should be uploaded instead of embedded as a data URL:
import { generateMetadata } from "gw-file/client";
const metadata = await generateMetadata(file, {
uploadBlob: async (posterBlob) => {
const result = await uploader.uploadBlob(posterBlob, "poster.jpg");
if (result.isErr) {
throw result.error;
}
return { src: cdn(result.value.key) };
},
});Direct server uploads
const result = await fileService.put(buffer, {
name: "report.pdf",
type: "application/pdf",
size: buffer.byteLength,
userId,
});For a Blob, use fileService.putBlob(blob, params).
CDN URLs
import { createCDN } from "gw-file";
const cdn = createCDN("https://cdn.example.com");
cdn("user/id/photo.jpg");
// https://cdn.example.com/user/id/photo.jpg
cdn("user/id/photo.jpg", { width: 640 });
// https://cdn.example.com/user/id/photo.jpg?w=640Responsive images
import { createResponsiveImage } from "gw-file/client";
const ResponsiveImage = createResponsiveImage({
cdnOrigin: "https://cdn.example.com",
defaultProps: { loading: "lazy" },
sizes: [320, 640, 960, 1280],
});
export function Avatar({ file }: { file: { key: string } }) {
return <ResponsiveImage file={file} alt="Profile" width={320} height={320} />;
}Omit sizes to use the package defaults. GIF images are returned without a generated srcSet so their animation is preserved.
AWS permissions
Grant only the operations used by the application. A typical policy for the default user prefix is:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::YOUR_BUCKET/user/*"
}
]
}- Presigned browser uploads require
s3:PutObject. ObjectStorage.findrequiress3:GetObject.ObjectStorage.headnormally requiress3:GetObject.ObjectStorage.deleterequiress3:DeleteObject.
Bucket policy, KMS encryption, VPC endpoint policy, and organization service-control policies can further restrict these operations.
S3 CORS
Browser-to-S3 uploads need a bucket CORS rule. Restrict origins in production:
[
{
"AllowedOrigins": ["https://app.example.com"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["content-type", "x-amz-*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]CORS errors happen before or while the browser accesses the response. An S3 XML response with HTTP 403 generally points to credentials, IAM or bucket policy, an expired URL, a region mismatch, or a signature mismatch.
Environment variables
These names are examples; gw-file does not read application-specific bucket variables itself:
AWS_REGION=ap-northeast-2
AWS_S3_BUCKET_NAME=your-bucket
AWS_ACCESS_KEY_ID=use-a-secret-store-in-production
AWS_SECRET_ACCESS_KEY=use-a-secret-store-in-productionThe AWS SDK reads its standard credential and region variables. Keep .env files out of source control and rotate exposed access keys immediately.
Public API summary
gw-file
createCDN(origin)CDN
gw-file/client
FileUploadergenerateMetadatacreateResponsiveImageResponsiveImagegenerateSrcSet
gw-file/server
ObjectStorageFileServiceFileRepositoryuploadFileHandlerdeleteFileHandler
Generated declaration files include JSDoc for detailed parameters and behavior.
Deployment checklist
- Set
AWS_REGIONexplicitly or passregiontoObjectStorage. - Provide credentials through an IAM role or secure deployment secret.
- Grant the minimum S3 actions for the configured key prefix.
- Configure S3 CORS for every browser origin that performs direct uploads.
- Configure a CDN or object URL strategy for reading uploaded files.
- Decide how abandoned repository references and orphaned S3 objects are cleaned up.
- Run
npm run checkand inspectnpm pack --dry-runbefore publishing.
Version 0.1.6
- Removed the package-owned
ap-northeast-2default; AWS region resolution is now explicit or delegated to the AWS SDK provider chain. - Added complete setup, IAM, CORS, lifecycle, and deployment documentation.
- Added JSDoc to the public API.
- Corrected package repository and module metadata.
- Upgraded
gw-resultto0.3.xanduuidto14.x.
License
MIT
