Skip to main content
The Cat Data API implements a dual-storage approach for images: files are stored on the filesystem while metadata is tracked in the PostgreSQL database.

Storage Architecture

1

File Upload

Images are uploaded via the /api/upload endpoint using multipart form data.
2

File Validation

Multer validates the file type, accepting only JPEG and PNG images.
3

Filesystem Storage

The file is saved to the images/ directory with a UUID-based filename.
4

Database Record

A record is created in the images table with the filename for future retrieval.

Multer Configuration

The API uses Multer for handling multipart/form-data file uploads.

Storage Location

src/index.ts
const imagesDir = path.join(__dirname, '../images');
if (!fs.existsSync(imagesDir)) fs.mkdirSync(imagesDir);
The images directory is created automatically at startup if it doesn’t exist. Files are stored at images/ relative to the project root.

File Filter

Only JPEG and PNG images are accepted:
src/index.ts
const fileFilter: multer.Options['fileFilter'] = (req, file, cb) => {
  const allowedTypes = ['image/jpeg', 'image/png'];
  if (!allowedTypes.includes(file.mimetype)) {
    return cb(new Error('Only JPG and PNG files are allowed.'));
  }
  cb(null, true);
};
Attempting to upload files with other MIME types will result in an error response. The validation happens before the file is written to disk.

Storage Strategy

Filenames are generated using UUIDs to prevent collisions:
src/index.ts
const storage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, imagesDir),
  filename: (req, file, cb) => cb(null, uuidv4() + '-' + file.originalname),
});

const upload = multer({ storage, fileFilter });
The filename format is: {uuid}-{original-filename} For example: a3d5e6f7-1234-5678-abcd-9876543210fe-cat-photo.jpg

Upload Endpoint

The upload endpoint requires authentication and processes a single file:
src/index.ts
app.post(
  '/api/upload',
  checkJwt,
  upload.single('file'),
  async (req, res): Promise<void> => {

    if (!req.file) {
      res.status(400).json({ error: 'Missing file to upload' });
      return;
    }

    const id = await addImage(req.file.filename);

    res.status(201).json({ id, filename: req.file.filename });
  }
);
The upload.single('file') middleware expects the file to be sent with the field name file in the multipart form data.
Request:
  • Method: POST
  • Endpoint: /api/upload
  • Headers: Authorization: Bearer {token}
  • Body: multipart/form-data with field name file
Response:
{
  "id": 1,
  "filename": "a3d5e6f7-1234-5678-abcd-9876543210fe-cat-photo.jpg"
}

Image Retrieval

Images are served directly from the filesystem:
src/index.ts
app.get('/api/image/:id', checkJwt, async (req, res) => {
  const id = Number(req.params.id);
  const fileInfo = await getImage(id);
  res.sendFile(path.join(imagesDir, fileInfo.name));
});
The endpoint:
  1. Looks up the filename in the database by ID
  2. Constructs the full file path
  3. Sends the file using Express’s sendFile() method

Image Deletion

Deleting an image removes both the database record and the file:
src/index.ts
app.delete('/api/image/:id', checkJwt, async (req, res) => {
  const id = Number(req.params.id);
  const fileInfo = await getImage(id);
  // delete file
  fs.unlinkSync(path.join(imagesDir, fileInfo.name));
  // delete from db
  await deleteImage(id);
  res.status(204).end();
});
Deletion is permanent and cannot be undone. The file is removed from the filesystem synchronously using fs.unlinkSync().

List All Images

Retrieve metadata for all stored images:
src/index.ts
app.get('/api/images', checkJwt, async (req, res) => {
  const images = await getAllImages();
  res.json(images);
});
Response:
[
  {
    "id": 1,
    "name": "a3d5e6f7-1234-5678-abcd-9876543210fe-cat-photo.jpg"
  },
  {
    "id": 2,
    "name": "b4e6f7g8-2345-6789-bcde-8765432109ed-kitten.png"
  }
]
This endpoint returns only metadata. To retrieve the actual image files, use the /api/image/:id endpoint.

Security Considerations

All image endpoints require JWT authentication via the checkJwt middleware. Unauthenticated requests will be rejected.
Only JPEG and PNG files are accepted. This is validated by checking the MIME type before storage.
Using UUIDs prevents filename collisions and makes it difficult to guess image URLs, providing some level of obscurity.

Build docs developers (and LLMs) love