abimongo-graphql
title: Abimongo + GraphQL (Next.js) — Multi-tenant starter description: A copy-pasteable Next.js + GraphQL tutorial showing how to wire Abimongo into a multi-tenant GraphQL API and a minimal React frontend.
Goal
This tutorial shows a minimal, copy-pasteable Next.js application that exposes a GraphQL API backed by Abimongo and implements multi-tenancy.
- API: Apollo Server (via
apollo-server-micro) running inpages/api/graphql.ts. - Multi-tenancy: tenants are registered at startup with Abimongo's multi-tenant manager and the request tenant is set using
TenantContext.run()so all downstream Abimongo calls resolve to the tenant DB. - Frontend: a tiny React page that calls the GraphQL API and demonstrates tenant isolation.
You can copy the whole project into a fresh folder and run it locally. The examples use JavaScript for simplicity.
Prerequisites
- Node 18+ (or current LTS)
- A MongoDB server for each tenant (local
mongodis fine). For local testing you can use a single MongoDB instance and separate databases/URIs per tenant. @abimongo/coreavailable in your environment (either published or linked from your workspace). If you're testing locally, publish to your local registry ornpm linkthe package.
Project layout (what we'll create)
- package.json
- .env.local
- lib/abimongo.js — Abimongo initialization + tenant registration helper
- lib/schemas/user.js — AbimongoSchema for the
Usertype - pages/api/graphql.js — GraphQL API (Apollo Server micro) with tenant context middleware
- pages/index.js — Simple React UI that calls the GraphQL API
1) package.json
Create a minimal Next.js project and install the required deps.
package.json
{
"name": "abimongo-graphql-next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3000",
"build": "next build",
"start": "next start -p 3000"
},
"dependencies": {
"next": "^14.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"apollo-server-micro": "^4.0.0",
"graphql": "^16.0.0",
"cross-fetch": "^3.1.5",
"@abimongo/core": "^1.0.0"
}
}
Install:
npm install
If you're working inside the abimongo monorepo, you can instead pnpm add or use npm link so @abimongo/core resolves locally.
2) Environment variables
Create .env.local with your tenant URIs. We'll encode a small JSON map in TENANTS_JSON for convenience in this tutorial. Example:
PORT=3000
# JSON string mapping tenantId -> mongodb URI
TENANTS_JSON={"tenant-a":"mongodb://localhost:27017/tenant_a","tenant-b":"mongodb://localhost:27017/tenant_b"}
When using a cloud MongoDB provider, make sure each tenant uses a separate database/URI for isolation.
3) Abimongo bootstrap helper (lib/abimongo.js)
This file initializes Abimongo multi-tenancy at server start. It calls initMultiTenancy (which registers tenants) and exposes a convenience ensureAbimongo() function so our API route can await initialization.
Create lib/abimongo.js:
// lib/abimongo.js
import { initMultiTenancy, AbimongoGraphQL, TenantContext } from '@abimongo/core';
let initialized = false;
let graphQLInstance = null;
export async function ensureAbimongo() {
if (initialized) return { graphQLInstance };
const raw = process.env.TENANTS_JSON || '{}';
let tenants = {};
try {
tenants = JSON.parse(raw);
} catch (err) {
console.error('TENANTS_JSON parse error:', err.message);
throw err;
}
// initMultiTenancy accepts an object { tenantId: uri }
// lazy: false will connect immediately. Set lazy: true to defer.
await initMultiTenancy(tenants, { lazy: false });
// Create a single AbimongoGraphQL generator instance and keep it.
// The generated schema uses TenantContext internally (Abimongo resolvers pick it up).
graphQLInstance = new AbimongoGraphQL({ useRedis: false });
initialized = true;
return { graphQLInstance };
}
export { TenantContext };
Notes:
initMultiTenancy(tenants, { lazy: false })registers the tenants and connects immediately. In production you may prefer lazy connections or more advanced tenant lifecycle management.TenantContextis exported so our request handler can run each request inside the correct tenant context.
4) Define your Abimongo schema (lib/schemas/user.js)
Create a small user schema using AbimongoSchema and export it. This same schema is used by the GraphQL generator.
// lib/schemas/user.js
import { AbimongoSchema } from '@abimongo/core';
export const UserSchema = new AbimongoSchema({
name: String,
email: String,
age: Number,
createdAt: Date,
});
// Optional: add a simple GC config
UserSchema.getGCConfig = () => ({ ttlField: 'createdAt' });
export default UserSchema;
5) GraphQL API (pages/api/graphql.js)
This is the Next.js API route that mounts Apollo Server. Important details:
- We
ensureAbimongo()at module scope to register tenants and create the AbimongoGraphQL instance. That step is async; we await it in the handler when needed. - For each incoming request we extract the tenant id from the
x-tenant-idheader, run the handler insideTenantContext.run(tenantId, ...)so all Abimongo resolvers and helpers resolve to that tenant's DB.
Create pages/api/graphql.js:
// pages/api/graphql.js
import { ApolloServer } from 'apollo-server-micro';
import { json } from 'micro';
import Cors from 'micro-cors';
import { ensureAbimongo, TenantContext } from '../../lib/abimongo';
let apolloServer = null;
let serverStarted = false;
const cors = Cors();
async function getApolloServer() {
if (apolloServer) return apolloServer;
const { graphQLInstance } = await ensureAbimongo();
const schema = await graphQLInstance.generateSchema();
apolloServer = new ApolloServer({
schema,
});
// Start Apollo (required by v4 before using the handler)
if (!serverStarted) {
await apolloServer.start();
serverStarted = true;
}
return apolloServer;
}
const handler = async (req, res) => {
if (req.method === 'OPTIONS') return res.end();
const tenantId = req.headers['x-tenant-id'] || req.query.tenant || 'tenant-a';
if (!tenantId) {
res.statusCode = 400;
return res.end('Tenant id required via x-tenant-id header');
}
// Ensure Abimongo + GraphQL are initialized
const server = await getApolloServer();
// Run inside the tenant context so Abimongo resolvers operate against the tenant DB
return TenantContext.run(tenantId, async () => {
const apolloHandler = server.createHandler({ path: '/api/graphql' });
return apolloHandler(req, res);
});
};
export const config = {
api: {
bodyParser: false,
},
};
export default cors(async (req, res) => {
// body parsing must be handled by Apollo; just pass through
await handler(req, res);
});
Notes:
- We keep a singleton
ApolloServerinstance to avoid rebuilding the schema for every request. - The handler uses
TenantContext.run(tenantId, () => ...)so any Abimongo helper that usesTenantContext.getTenantId()will resolve correctly.
6) Minimal frontend (pages/index.js)
Create a tiny React page that allows switching tenant and creating / listing users.
// pages/index.js
import React, { useState, useEffect } from 'react';
const GRAPHQL = '/api/graphql';
const fetchGQL = async (tenantId, query, variables = {}) => {
const res = await fetch(GRAPHQL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-tenant-id': tenantId,
},
body: JSON.stringify({ query, variables }),
});
return res.json();
};
export default function Home() {
const [tenant, setTenant] = useState('tenant-a');
const [users, setUsers] = useState([]);
const [name, setName] = useState('Alice');
const loadUsers = async () => {
const q = `query { findUsers { id name email age createdAt } }`;
const r = await fetchGQL(tenant, q);
setUsers(r.data?.findUsers || []);
};
useEffect(() => { loadUsers(); }, [tenant]);
const addUser = async () => {
const mutation = `mutation($input: CreateUserInput!) { createUser(input: $input) { id name email age } }`;
const input = { name, email: `${name.toLowerCase()}@example.com`, age: 30 };
await fetchGQL(tenant, mutation, { input });
await loadUsers();
};
return (
<div style={{ padding: 24 }}>
<h1>Abimongo + GraphQL (multi-tenant)</h1>
<label>Tenant: </label>
<select value={tenant} onChange={e => setTenant(e.target.value)}>
<option value="tenant-a">tenant-a</option>
<option value="tenant-b">tenant-b</option>
</select>
<h2>Create user</h2>
<input value={name} onChange={e => setName(e.target.value)} />
<button onClick={addUser}>Create</button>
<h2>Users (tenant: {tenant})</h2>
<button onClick={loadUsers}>Refresh</button>
<ul>
{users.map(u => (
<li key={u.id}>{u.name} — {u.email} — {u.age}</li>
))}
</ul>
</div>
);
}
Notes:
- The GraphQL schema generated by
AbimongoGraphQLexposes simplefindUsersandcreateUseroperations for models that exist. The auto-generated schema may use different operation names depending on your Abimongo config; use GraphiQL orschemaintrospection to confirm.
7) Example GraphQL queries
- List users
query {
findUsers {
id
name
email
age
createdAt
}
}
- Create user (mutation)
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
}
}
When calling from the frontend or a client, be sure to include x-tenant-id as a header.
8) Run it
- Ensure MongoDB is running and you have the tenant URIs in
.env.local. - npm run dev
- Open
http://localhost:3000and choose tenants from the dropdown. Each tenant reads/writes its own DB.
If you prefer to experiment with GraphiQL, you can POST to /api/graphql with the x-tenant-id header or use an external client (Apollo Studio / GraphiQL) and set headers.
9) Implementation notes & best practices
- TenantContext.run vs setTenantId: Prefer
TenantContext.run(tenantId, () => ...)to ensure context flows across async calls.TenantContext.setTenantId()andclear()are ok for scripts and tests but not per-request handlers. - Connection lifecycle:
initMultiTenancy(..., { lazy: false })opens connections for all tenants at startup — good for small numbers of tenants. For many tenants, register tenants lazily withregisterLazyTenant/initMultiTenancy(..., { lazy: true })and connect on first use. - Security: Validate tenant ids and ensure the incoming
x-tenant-idis authorized for the calling client (e.g., via JWT claims). - Schema generation:
AbimongoGraphQLauto-generates CRUD operations for Abimongo models — you can extend or override the generated resolvers withcustomResolvers()or addcustomTypeDefs().
10) Troubleshooting
- If GraphQL schema generation fails, ensure
@abimongo/coreis installed and that theUserSchema(or any registered schema) is exported and imported beforegenerateSchema()runs. - If tenant models are missing, confirm
initMultiTenancy()ran and tenants are registered. Also confirmx-tenant-idheader matches a registered tenant id.