Supabase + TypeScript: Building Type-Safe APIs
Supabase provides excellent TypeScript support that, when combined with a clean architecture backend, creates a powerful and safe development experience. In this post, I will share how I built the API for my portfolio site using Cloudflare Workers, Supabase, and TypeScript.
Project Architecture
src/
├── domain/ # Business entities & repository interfaces
│ ├── entities/ # BlogPost, Project, User
│ └── repositories/ # Repository contracts (interfaces)
├── application/ # Use cases (business logic)
│ └── usecases/ # CreateBlogPost, GetProjects, etc.
├── infrastructure/ # External services implementation
│ └── supabase/ # Supabase repository implementations
└── interfaces/ # HTTP layer
└── http/ # Handlers, middleware, router
Generate Types from Your Database
The Supabase CLI generates TypeScript types directly from your database schema:
1npx supabase gen types typescript --project-id YOUR_PROJECT_ID > src/types/database.tsThis gives you full type safety for every table and column:
1export interface Database {
2 public: {
3 Tables: {
4 posts: {
5 Row: {
6 id: string;
7 title: string;
8 slug: string;
9 content: string;
10 status: 'draft' | 'published' | 'archived';
11 published_at: string | null;
12 author_id: string;
13 category_id: string | null;
14 reading_time: number | null;
15 };
16 Insert: { /* ... */ };
17 Update: { /* ... */ };
18 };
19 };
20 };
21}Repository Pattern with Type Safety
Define domain interfaces, then implement them with Supabase:
1// Domain interface
2interface BlogRepository {
3 findPostBySlug(slug: string): Promise<BlogPost | null>;
4 findPosts(options: PostQueryOptions): Promise<PaginatedResult<BlogPost>>;
5 createPost(data: CreatePostData): Promise<BlogPost>;
6}
7
8// Supabase implementation
9class SupabaseBlogRepository implements BlogRepository {
10 constructor(private client: SupabaseClient<Database>) {}
11
12 async findPostBySlug(slug: string): Promise<BlogPost | null> {
13 const { data, error } = await this.client
14 .from('posts')
15 .select(`
16 *,
17 authors ( id, name, avatar, bio ),
18 categories ( id, name, slug ),
19 post_tags ( tags ( id, name, slug ) )
20 `)
21 .eq('slug', slug)
22 .eq('status', 'published')
23 .single();
24
25 if (error || !data) return null;
26 return this.mapToPost(data);
27 }
28}Use Cases for Business Logic
Keep business rules separate from data access:
1class CreateBlogPost {
2 constructor(private repo: BlogRepository) {}
3
4 async execute(data: CreatePostInput): Promise<BlogPost> {
5 // Auto-generate slug from title
6 const slug = generateSlug(data.title);
7
8 // Calculate reading time
9 const readingTime = Math.ceil(data.content.split(/\s+/).length / 200);
10
11 // Auto-set publishedAt if status is published
12 const publishedAt = data.status === 'published' ? new Date().toISOString() : undefined;
13
14 return this.repo.createPost({
15 ...data,
16 slug,
17 readingTime,
18 publishedAt,
19 });
20 }
21}Cloudflare Workers as the Runtime
Cloudflare Workers provide edge computing with zero cold starts:
1export default {
2 async fetch(request: Request, env: Env): Promise<Response> {
3 const container = createContainer(env);
4 const router = new ModularRouter(container);
5 return router.handle(request);
6 },
7};Environment variables are configured in wrangler.jsonc:
1{
2 "name": "technical-backend-worker",
3 "vars": {
4 "SUPABASE_URL": "https://xxx.supabase.co",
5 "SUPABASE_ANON_KEY": "eyJ..."
6 }
7}Benefits of This Stack
- Type safety end-to-end — from DB schema to API responses
- Edge performance — Cloudflare Workers run globally with ~0ms cold starts
- Free tier friendly — both Supabase and Cloudflare have generous free tiers
- Clean architecture — easy to test, maintain, and extend
Conclusion
The combination of Supabase for data, TypeScript for safety, and Cloudflare Workers for edge performance creates a modern, fast, and maintainable backend. I have been running this exact stack for my portfolio site and it has been rock-solid.