Small NestJS REST API for uploading images and reading their metadata.
Uploaded images are resized/cropped with Sharp, stored locally as WebP files, and exposed under /uploads. Image metadata is stored in PostgreSQL.
- NestJS
- TypeScript
- PostgreSQL
- TypeORM
- Sharp
- Docker Compose
- Swagger / OpenAPI
- Jest
- Docker and Docker Compose
- Node.js 22 if running outside Docker
- npm
Use the project Node version:
nvm useCopy the example environment file:
cp .env.example .envInstall dependencies for local development:
npm install| Variable | Description | Default |
|---|---|---|
API_PORT |
Host port for the API container | 3000 |
POSTGRES_PORT |
Host port for PostgreSQL | 5432 |
DATABASE_HOST |
PostgreSQL host for local app runs | localhost |
DATABASE_PORT |
PostgreSQL port | 5432 |
DATABASE_USER |
PostgreSQL user | image_api |
DATABASE_PASSWORD |
PostgreSQL password | image_api |
DATABASE_NAME |
PostgreSQL database name | image_api |
PUBLIC_BASE_URL |
Base URL used when returning image URLs | http://localhost:3000 |
UPLOAD_DIR |
Directory for processed image files | uploads |
Start the API and PostgreSQL:
docker compose up --buildThe API runs at:
http://localhost:3000
Swagger is available at:
http://localhost:3000/docs
PostgreSQL is exposed on the configured POSTGRES_PORT.
Database migrations run automatically when the API starts. This keeps schema changes explicit while keeping local setup simple.
Uploaded files are stored in a Docker volume when running through Docker Compose.
Stop the stack:
docker compose downTo remove containers and volumes:
docker compose down -vOpenAPI documentation is available through Swagger UI:
http://localhost:3000/docs
curl -X POST http://localhost:3000/images \
-F "title=Profile photo" \
-F "width=800" \
-F "height=600" \
-F "file=@/path/to/image.jpg"Supported upload formats are:
jpgjpegpngwebp
The uploaded image is resized/cropped to the requested width and height before it is saved. Processed files are stored as WebP.
Example response:
{
"id": "2d69d8a8-51e9-447d-a0a0-eec4a2c99b7f",
"url": "http://localhost:3000/uploads/image.webp",
"title": "Profile photo",
"width": 800,
"height": 600
}The returned url can be opened in a browser to view the processed image.
curl "http://localhost:3000/images?page=1&limit=10"Example response:
{
"data": [
{
"id": "2d69d8a8-51e9-447d-a0a0-eec4a2c99b7f",
"url": "http://localhost:3000/uploads/image.webp",
"title": "Profile photo",
"width": 800,
"height": 600
}
],
"meta": {
"page": 1,
"limit": 10,
"total": 1,
"totalPages": 1
}
}curl "http://localhost:3000/images?title=profile&page=1&limit=10"The title filter matches records where the title contains the provided text. The match is case-insensitive.
curl "http://localhost:3000/images/2d69d8a8-51e9-447d-a0a0-eec4a2c99b7f"Example response:
{
"id": "2d69d8a8-51e9-447d-a0a0-eec4a2c99b7f",
"url": "http://localhost:3000/uploads/image.webp",
"title": "Profile photo",
"width": 800,
"height": 600
}If the image does not exist, the API returns 404.
Run PostgreSQL with Docker:
docker compose up postgresRun the API locally:
nvm use
npm install
npm run start:devIf the API is running outside Docker, make sure the DATABASE_* values point to the running PostgreSQL instance.
Run tests:
npm testRun lint and build checks:
npm run lint
npm run buildLocal filesystem storage was chosen to keep the project self-contained and easy to run locally. When using Docker Compose, uploaded files are stored in a Docker volume mounted to the API container.
The storage logic is isolated in LocalImagesStorageService, so it can be replaced later with S3, Azure Blob Storage, Google Cloud Storage, or another object storage provider without changing the controller layer.
- There is no authentication or authorization.
- There is no delete endpoint or metadata update endpoint.
- File size limits are not configured yet.
- Tests focus on controller and service behavior. Full image processing and filesystem writes can be verified through Docker/manual upload checks.
- Add object storage support.
- Add file size limits and stricter upload constraints.
- Add delete and update endpoints.
- Add request logging and production-ready observability.
- Add authentication if the API is exposed outside local development.