Set Up Itty-router & API Middleware For Cloudflare

by SLV Team 51 views
Setting Up itty-router and API Middleware Structure

Hey guys! Let's dive into setting up a robust routing system and middleware for our Image API. We're going to use itty-router, which is perfect for Cloudflare Workers. This guide will walk you through the process, ensuring you have a solid foundation for handling requests, implementing CORS, logging, and error handling. This will make our API more flexible and efficient. Let's get started, it's going to be a fun ride!

Refactoring the Main Worker Entry Point

First off, we'll convert our apps/api/src/index.ts from having inline handlers to using a router-based structure. This change is crucial for managing our API endpoints cleanly and efficiently, especially as we add more features. Imagine having a simple, readable structure that helps you manage all of your API's functions! This is what the router will do for you. Think of it like this: the router is the traffic controller for your API, directing each request to the right place. By moving to a router-based approach, we're setting up the foundation for a more scalable and maintainable API.

import { Router, error, json } from 'itty-router';
import { connectD1 } from '@app/db/client';

const router = Router();

// Middleware
router.all('*', corsMiddleware);
router.all('*', loggerMiddleware);

// Health check (keep existing)
router.get('/health', () => json({ ok: true, ts: Date.now() }));

// API routes (to be implemented in future issues)
router.get('/api/v1/images', listImages);
router.get('/api/v1/images/:id', getImage);
router.post('/api/v1/images', uploadImage);
router.patch('/api/v1/images/:id', updateImage);
router.delete('/api/v1/images/:id', deleteImage);

// 404 handler
router.all('*', () => error(404, 'Not Found'));

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    return router.fetch(request, env);
  }
} satisfies ExportedHandler<Env>;

In this code, we initialize the router using itty-router. We then define middleware for handling CORS and logging using corsMiddleware and loggerMiddleware, respectively. We also include a health check endpoint, /health, and placeholder API routes. The router.all('*', () => error(404, 'Not Found')) part is super important—it makes sure that if someone tries to reach a non-existent route, they get a helpful 'Not Found' error.

Creating the Middleware File

Next, we need to create apps/api/src/middleware.ts. This file will hold our middleware functions. Middleware allows us to handle things like CORS (Cross-Origin Resource Sharing), logging, and environment setup before requests reach the actual route handlers. Think of middleware as layers of processing that happen before and/or after your main API functions are executed. It's an awesome way to keep your code organized and reusable!

import { IRequest } from 'itty-router';

export function corsMiddleware(request: IRequest) {
  // Add CORS headers
  const corsHeaders = {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
  };

  if (request.method === 'OPTIONS') {
    return new Response(null, { headers: corsHeaders });
  }

  // Attach CORS headers to response (handled by router)
  request.corsHeaders = corsHeaders;
}

export function loggerMiddleware(request: IRequest) {
  const start = Date.now();
  console.log(`[${request.method}] ${request.url}`);

  // Attach timing info
  request.startTime = start;
}

export function attachEnv(env: Env) {
  return (request: IRequest) => {
    request.env = env;
  };
}

The corsMiddleware function adds the necessary CORS headers to allow requests from different origins. This is crucial for enabling cross-origin requests, which is pretty much essential if you want your API to be accessible from different websites or applications. The loggerMiddleware function logs the HTTP method and the URL of each request, which helps you debug and monitor your API. Finally, attachEnv is useful to pass the environment variables into the request, useful if you need to use a D1 database or an R2 bucket.

Setting up Route Handlers Placeholder

We're now going to create the file apps/api/src/routes/images.ts. This is where we'll put all of our route handler functions. Even though, at this stage, they'll be placeholder functions, we're preparing the structure. This separation makes our code more organized and easier to maintain. This approach keeps your code clean and makes it easier to add new features later on.

import { IRequest, json } from 'itty-router';

// Placeholder handlers (to be implemented in future issues)
export async function listImages(request: IRequest) {
  return json({ success: true, data: { images: [], pagination: {} } });
}

export async function getImage(request: IRequest) {
  return json({ success: true, data: { id: request.params?.id } });
}

export async function uploadImage(request: IRequest) {
  return json({ success: true, message: 'Upload endpoint - to be implemented' }, 201);
}

export async function updateImage(request: IRequest) {
  return json({ success: true, message: 'Update endpoint - to be implemented' });
}

export async function deleteImage(request: IRequest) {
  return json({ success: true, message: 'Delete endpoint - to be implemented' });
}

Each function in this file will correspond to a specific API endpoint. We have placeholders for listImages, getImage, uploadImage, updateImage, and deleteImage. Each of these functions currently returns a JSON response with a success message, but later on, we'll replace these placeholders with actual functionality to interact with the image data.

Updating Type Definitions

To make our code even better, let's create a file called apps/api/src/types.ts. This file will hold our type definitions. This helps ensure that the data we're working with is consistent and helps prevent errors. Typing is super important – it helps you catch errors early and makes your code much more predictable.

export interface Env {
  DB: D1Database;
  R2_BUCKET: R2Bucket;
  METADATA_QUEUE: Queue<MetadataJob>;
}

export interface MetadataJob {
  imageId: string;
  cloudflareImageId: string;
  originalFilename: string;
}

Here, we're defining the Env interface, which includes the database (DB), R2 bucket (R2_BUCKET), and the metadata queue (METADATA_QUEUE). We're also defining a MetadataJob interface. These interfaces specify the types of objects that we'll be working with in our API. This means that if something doesn't align with the types defined, the TypeScript compiler will immediately flag it, preventing errors.

Steps to Implement

Let's get down to the implementation part! Here are the steps to create the necessary files and integrate the itty-router.

  1. Create the middleware file:

    touch apps/api/src/middleware.ts
    
  2. Create the routes directory and files:

    mkdir -p apps/api/src/routes
    touch apps/api/src/routes/images.ts
    
  3. Create the types file:

    touch apps/api/src/types.ts
    
  4. Refactor apps/api/src/index.ts to use the router:

    Copy the code mentioned above.

  5. Test all endpoints:

    pnpm --dir apps/api dev
    
    # Test health
    curl http://127.0.0.1:8787/health
    
    # Test CORS preflight
    curl -X OPTIONS http://127.0.0.1:8787/api/v1/images \
      -H "Access-Control-Request-Method: POST"
    
    # Test 404
    curl http://127.0.0.1:8787/nonexistent
    

Testing Your Work

To ensure everything is working correctly, let's create a test file named apps/api/test/router.test.ts. This will allow us to test the functionality of our router and its middleware. Tests help us catch errors early and make sure our API behaves as expected.

import { describe, it, expect } from 'vitest';
import worker from '../src/index';

describe('Router', () => {
  it('should return health check', async () => {
    const req = new Request('http://localhost/health');
    const env = getMockEnv();
    const res = await worker.fetch(req, env);

    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data.ok).toBe(true);
  });

  it('should handle CORS preflight', async () => {
    const req = new Request('http://localhost/api/v1/images', {
      method: 'OPTIONS'
    });
    const env = getMockEnv();
    const res = await worker.fetch(req, env);

    expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*');
  });

  it('should return 404 for unknown routes', async () => {
    const req = new Request('http://localhost/nonexistent');
    const env = getMockEnv();
    const res = await worker.fetch(req, env);

    expect(res.status).toBe(404);
  });
});

This test suite covers the health check, CORS preflight requests, and the 404 error handling. By running these tests, you can verify that your router is correctly handling different types of requests and responses. The tests will help you be confident that your setup works as expected.

Dependencies

  • Depends on: #3 (npm dependencies must be installed)

Wrapping Up

Alright, guys, you've now set up a solid foundation for your Image API using itty-router, middleware, and route handlers. This structure is flexible, scalable, and easy to maintain, providing a great starting point for building out more complex API functionality. Keep in mind that this is just the beginning – you'll need to fill in the placeholders with the actual logic to handle image uploads, retrieval, and other operations. But with this setup, you're well on your way to building a powerful and efficient API. Great job!