Stack logo
Sync up on the latest from Convex.
Jordan Hunt's avatar
Jordan Hunt
2 months ago

Convex in Multiple Repositories

Image of a branch, referencing a github repository, next to a shield, which represents type safety.

Have you ever wanted to use your Convex functions in a different repository than where you define them? with type-safety? Well.. look no further.

Convex recently released the ability to generate a TypeScript API specification from your function metadata, which enables this use-case. Some scenarios in which this would useful are collaborating with frontend developers or contractors in a separate repository, having multiple product surfaces (admin vs. main), and having client implementations in separate repositories.

Below, I will dive into an example of what this workflow could look like. To get started, you should install the “Convex Helpers” library using npm install convex-helpers and define validators on all your Convex functions.

Using Convex within multiple repositories

Previously, it was hard to use Convex functions in a type-safe way outside of the repository where your Convex functions are defined. Now, we provide you a way to generate a file similar to convex/_generated/api.d.ts that you can use in separate repositories.

1. Generate an api.ts file

You can run:

npx convex-helpers ts-api-spec

to generate a TypeScript API file for your Convex deployment. Below is an example of a Convex function definition and the corresponding API file. Your generated file will look something like the api.ts file below.

// api.ts (generated API file)
import { FunctionReference, anyApi } from "convex/server";
import { GenericId as Id } from "convex/values";

export const api: PublicApiType = anyApi as unknown as PublicApiType;
export const internal: InternalApiType = anyApi as unknown as InternalApiType;

export type PublicApiType = {
  messages: {
    list: FunctionReference<
      "query",
      "public",
      Record<string, never>,
      Array<{
        _creationTime: number;
        _id: Id<"messages">;
        author: string;
        body: string;
      }>
    >;
    send: FunctionReference<
      "mutation",
      "public",
      { author: string; body: string },
      null
    >;
  };
};
export type InternalApiType = {};

The types in this example come from a convex/messages.ts file like:

// convex/messages.ts (function definition)
export const list = query({
  args: {},
  returns: v.array(
    v.object({
      body: v.string(),
      author: v.string(),
      _id: v.id("messages"),
      _creationTime: v.number(),
    }),
  ),
  handler: async (ctx) => {
    return await ctx.db.query("messages").collect();
  },
});

export const send = mutation({
  args: { body: v.string(), author: v.string() },
  returns: v.null(),
  handler: async (ctx, { body, author }) => {
    const message = { body, author };
    await ctx.db.insert("messages", message);
  },
});

2. Install Convex in a separate repository

Once you generate this file, you can use it in any other repository you want to use your Convex functions in. You must also install the Convex package in this other repository using

npm install convex

The most common use-case for this is having your frontend code exist in a separate repository than the code for your Convex deployment.

3. Connect to your backend from the separate repository

We must ensure that your frontend code is connecting to the correct Convex deployment. You can do this by setting your deployment URL as an environment variable when you create your Convex client. The example below is for React (Vite). See the Quickstarts for details on how to configure clients for other frameworks.

import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { ConvexProvider, ConvexReactClient } from "convex/react";

const address = import.meta.env.VITE_CONVEX_URL as string;

const convex = new ConvexReactClient(address);

ReactDOM.createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <ConvexProvider client={convex}>
      <App />
    </ConvexProvider>
  </StrictMode>,
);

4. Use api.ts from the separate repository

Once you have this api.ts copied into another repository, you can use it with the Convex client to call any of the Convex functions with type safety. Below is an example App.tsx file that imports from the copied-over api.ts file.

// src/App.tsx
import { FormEvent, useState } from "react";
import { useMutation, useQuery } from "convex/react";
// Note: we are importing from `../api` not `../convex/_generated/api`
import { api } from "../api";

export default function App() {
  const messages = useQuery(api.messages.list) || [];

  const [newMessageText, setNewMessageText] = useState("");
  const sendMessage = useMutation(api.messages.send);

  const [name] = useState(() => "User " + Math.floor(Math.random() * 10000));
  async function handleSendMessage(event: FormEvent) {
    event.preventDefault();
    await sendMessage({ body: newMessageText, author: name });
    setNewMessageText("");
  }
  return (
    <main>
      <h1>Convex Chat</h1>
      <p className="badge">
        <span>{name}</span>
      </p>
      <ul>
        {messages.map((message) => (
          <li key={message._id}>
            <span>{message.author}:</span>
            <span>{message.body}</span>
            <span>{new Date(message._creationTime).toLocaleTimeString()}</span>
          </li>
        ))}
      </ul>
      <form onSubmit={handleSendMessage}>
        <input
          value={newMessageText}
          onChange={(event) => setNewMessageText(event.target.value)}
          placeholder="Write a message…"
        />
        <input type="submit" value="Send" disabled={!newMessageText} />
      </form>
    </main>
  );
}

Now your frontend code is talking to your backend in a separate repository!

Notes

  • Argument and return value validators are not required, but the generated specs will only be as good as the validators provided. Convex validators (things like v.string()) are how Convex provides both runtime validation and provides typesafe APIs to clients. For this API generation to work the best, you’ll want to define both args and returns validators to provide the types.
  • When you update your Convex backend and want to use the updated functions, you’ll need to re-generate the api.ts file. We suggest making this process part of your deployment workflow.

Check out the docs here. I am excited to see what you build with this new functionality!

Build in minutes, scale forever.

Convex is the sync platform with everything you need to build your full-stack project. Cloud functions, a database, file storage, scheduling, search, and realtime updates fit together seamlessly.

Get started