본문으로 건너뛰기 Complete Convex Complete Guide | Realtime Backend

Complete Convex Complete Guide | Realtime Backend

Complete Convex Complete Guide | Realtime Backend

이 글의 핵심

Complete guide to building realtime backend with Convex. From type-safe APIs, realtime subscriptions, file storage to authentication with practical exam...

Key Takeaways

Complete guide to building realtime backend with Convex. From type-safe APIs, realtime subscriptions, file storage to authentication with practical examples.

Real-World Experience: Sharing experience of switching from Firebase to Convex, improving type safety and making realtime features more powerful.

Introduction: “Realtime Backend Is Complex”

Real-World Problem Scenarios

Scenario 1: Lack of Type Safety
Firebase has weak typing. Convex provides perfect type safety. Scenario 2: Difficult Realtime Subscriptions
WebSocket setup is complex. Convex handles it automatically. Scenario 3: Need Backend Logic
Difficult to handle on client. Convex provides server functions.

1. What is Convex?

Core Features

Convex is a realtime backend platform. Key Advantages:

  • Type Safety: End-to-End TypeScript
  • Realtime: Automatic subscriptions
  • Server Functions: Query, Mutation, Action
  • File Storage: Built-in
  • Authentication: Clerk integration

2. Project Setup

Installation

npm create convex@latest

Project Structure

my-convex-app/
├── convex/
│   ├── schema.ts
│   ├── users.ts
│   └── posts.ts
├── src/
│   └── app/
└── convex.json

3. Schema Definition

// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
export default defineSchema({
  users: defineTable({
    email: v.string(),
    name: v.string(),
    createdAt: v.number(),
  }).index('by_email', ['email']),
  posts: defineTable({
    title: v.string(),
    content: v.string(),
    authorId: v.id('users'),
    published: v.boolean(),
    createdAt: v.number(),
  })
    .index('by_author', ['authorId'])
    .index('by_published', ['published']),
});

4. Query & Mutation

Query

// convex/posts.ts
import { query } from './_generated/server';
import { v } from 'convex/values';
export const list = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query('posts').collect();
  },
});
export const get = query({
  args: { id: v.id('posts') },
  handler: async (ctx, args) => {
    return await ctx.db.get(args.id);
  },
});

Mutation

import { mutation } from './_generated/server';
import { v } from 'convex/values';
export const create = mutation({
  args: {
    title: v.string(),
    content: v.string(),
    authorId: v.id('users'),
  },
  handler: async (ctx, args) => {
    const postId = await ctx.db.insert('posts', {
      ...args,
      published: false,
      createdAt: Date.now(),
    });
    return postId;
  },
});
export const update = mutation({
  args: {
    id: v.id('posts'),
    title: v.string(),
  },
  handler: async (ctx, args) => {
    await ctx.db.patch(args.id, { title: args.title });
  },
});

5. React Integration

Provider

// app/ConvexClientProvider.tsx
'use client';
import { ConvexProvider, ConvexReactClient } from 'convex/react';
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export default function ConvexClientProvider({ children }) {
  return <ConvexProvider client={convex}>{children}</ConvexProvider>;
}

useQuery

'use client';
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
export default function Posts() {
  const posts = useQuery(api.posts.list);
  if (posts === undefined) return <div>Loading...</div>;
  return (
    <ul>
      {posts.map((post) => (
        <li key={post._id}>{post.title}</li>
      ))}
    </ul>
  );
}

useMutation

'use client';
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
export default function CreatePost() {
  const createPost = useMutation(api.posts.create);
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    await createPost({
      title: formData.get('title') as string,
      content: formData.get('content') as string,
      authorId: 'user-id',
    });
  };
  return (
    <form onSubmit={handleSubmit}>
      <input name="title" required />
      <textarea name="content" />
      <button type="submit">Create Post</button>
    </form>
  );
}

6. Action (External API)

// convex/actions.ts
import { action } from './_generated/server';
import { v } from 'convex/values';
export const sendEmail = action({
  args: {
    to: v.string(),
    subject: v.string(),
    body: v.string(),
  },
  handler: async (ctx, args) => {
    const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        personalizations: [{ to: [{ email: args.to }] }],
        from: { email: 'noreply@example.com' },
        subject: args.subject,
        content: [{ type: 'text/plain', value: args.body }],
      }),
    });
    return response.ok;
  },
});

7. File Storage

// convex/files.ts
import { mutation } from './_generated/server';
export const generateUploadUrl = mutation({
  args: {},
  handler: async (ctx) => {
    return await ctx.storage.generateUploadUrl();
  },
});
export const saveFile = mutation({
  args: { storageId: v.string() },
  handler: async (ctx, args) => {
    await ctx.db.insert('files', {
      storageId: args.storageId,
      createdAt: Date.now(),
    });
  },
});
// Client
const generateUploadUrl = useMutation(api.files.generateUploadUrl);
const saveFile = useMutation(api.files.saveFile);
const handleUpload = async (file: File) => {
  const uploadUrl = await generateUploadUrl();
  const response = await fetch(uploadUrl, {
    method: 'POST',
    body: file,
  });
  const { storageId } = await response.json();
  await saveFile({ storageId });
};

Summary and Checklist

Key Summary

  • Convex: Realtime backend
  • Type Safety: End-to-End TypeScript
  • Realtime: Automatic subscriptions
  • Server Functions: Query, Mutation, Action
  • File Storage: Built-in
  • Authentication: Clerk integration

Implementation Checklist

  • Create Convex project
  • Define schema
  • Implement queries
  • Implement mutations
  • Integrate React
  • Implement actions
  • Implement file storage

  • Complete Supabase Guide
  • Complete tRPC Guide
  • Complete Firebase Guide

Keywords Covered

Convex, Backend, Realtime, TypeScript, React, Serverless, Database

Frequently Asked Questions (FAQ)

Q. How does it compare to Firebase?

A. Convex has much better type safety. Firebase is more mature with more features.

Q. How does it compare to Supabase?

A. Convex has more powerful realtime features. Supabase is PostgreSQL-based and more flexible.

Q. Can I use it for free?

A. Yes, there’s a free plan. Free up to 1GB data and 1GB file storage.

Q. Is it safe to use in production?

A. Yes, many startups are using it.


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Complete guide to building realtime backend with Convex.

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers Convex, Backend, Realtime, TypeScript, React, Serverless, Database.