Install
$ agentstack add skill-mrlynn-claude-skills-mongodb-nextjs-scaffold ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
mongodb-nextjs-scaffold
Trigger
Use this skill when bootstrapping a new Next.js project with MongoDB, or when setting up the foundation layer (database connection, theme, response utilities, validation) for a MongoDB DevRel application.
> This is where every project starts for me. I got tired of re-explaining "use a singleton connection" and "wrap your API responses" so now Claude just knows. — ML
Overview
Every MongoDB DevRel project starts the same way: Next.js App Router + Mongoose connection pooling + MUI theme with MongoDB branding + standard API route patterns. This skill generates that foundation so you never re-derive it.
How to Use
Quick Start
Invoke with /mongodb-nextjs-scaffold or let Claude auto-activate when bootstrapping a new Next.js + MongoDB project.
Python Tools
scripts/brand_color_checker.py— Validate CSS/theme files against MongoDB brand palettescripts/env_checker.py— Check .env has all required variables for selected skills
Reference Docs
references/project-structure.md— Full directory tree for a scaffolded project
Templates & Samples
assets/sample.env— Template .env with all variables documentedassets/api-route-template.ts— Copy-paste starter for new API routes
Architecture Decisions
- Next.js App Router (not Pages Router): All new projects use the App Router with
(app)route groups for layout isolation. - Mongoose over native driver: Mongoose provides schema validation, middleware hooks, and model registration patterns that prevent common bugs in serverless environments.
- Singleton connection with global cache: In serverless/edge environments, each function invocation could create a new connection. The global cache pattern prevents connection leaks by reusing connections across hot reloads and concurrent requests.
- MUI with MongoDB branding: Material-UI provides accessible, responsive components out of the box. The theme is configured with MongoDB Brand Book V4.0 tokens — not generic colors.
- Zod for runtime validation: TypeScript types disappear at runtime. Zod schemas validate API inputs and provide typed parse results.
- JWT session strategy: JWT sessions are stateless and edge-compatible. Database sessions require a DB round-trip on every request.
File Structure
src/
├── app/
│ ├── (app)/
│ │ ├── layout.tsx # Main app layout with ThemeRegistry + SessionProvider
│ │ ├── page.tsx # Home page
│ │ └── admin/
│ │ └── layout.tsx # Admin layout with guard
│ ├── api/
│ │ └── auth/
│ │ └── [...nextauth]/
│ │ └── route.ts # NextAuth route handler
│ └── layout.tsx # Root layout (html, body, fonts)
├── components/
│ └── shared-ui/
│ └── ThemeRegistry.tsx # SSR Emotion cache + ThemeProvider
├── lib/
│ ├── db/
│ │ ├── connection.ts # Singleton Mongoose connection
│ │ ├── models/ # Mongoose models
│ │ │ └── User.ts
│ │ └── schemas.ts # Zod validation schemas
│ ├── auth.ts # NextAuth configuration
│ └── utils.ts # Response helpers
├── styles/
│ └── theme.ts # MongoDB brand theme
└── types/
└── next-auth.d.ts # NextAuth type extensions
Code Patterns
Pattern 1: MongoDB Singleton Connection
This is the most critical pattern. Without it, you'll leak connections in serverless environments.
// src/lib/db/connection.ts
import mongoose from "mongoose";
interface MongooseCache {
conn: typeof mongoose | null;
promise: Promise | null;
}
declare global {
// eslint-disable-next-line no-var
var mongoose: MongooseCache | undefined;
}
const cached: MongooseCache = global.mongoose ?? { conn: null, promise: null };
if (!global.mongoose) {
global.mongoose = cached;
}
export async function connectToDatabase(): Promise {
if (cached.conn) {
return cached.conn;
}
const MONGODB_URI = process.env.MONGODB_URI;
if (!MONGODB_URI) {
throw new Error(
"Please define the MONGODB_URI environment variable inside .env.local"
);
}
if (!cached.promise) {
cached.promise = mongoose
.connect(MONGODB_URI, { bufferCommands: false })
.then((m) => m);
}
cached.conn = await cached.promise;
return cached.conn;
}
Why bufferCommands: false? Mongoose buffers model operations until a connection is established. In serverless, this can cause operations to hang if the connection fails silently. Disabling it makes failures explicit.
Why cache both conn and promise? If two concurrent requests hit the server before the connection resolves, without caching the promise, both would call mongoose.connect() — creating duplicate connections.
Pattern 2: Mongoose Model Registration Guard
Mongoose throws OverwriteModelError if you call mongoose.model() twice with the same name. In development with hot module reload, this happens on every save.
// src/lib/db/models/User.ts
import mongoose, { Schema, Document, Types } from "mongoose";
export interface IUser extends Document {
email: string;
name: string;
role: "super_admin" | "admin" | "organizer" | "marketer" | "mentor" | "judge" | "participant" | "partner";
// ... additional fields
createdAt: Date;
updatedAt: Date;
}
const UserSchema = new Schema(
{
email: { type: String, required: true, unique: true, lowercase: true },
name: { type: String, required: true },
role: {
type: String,
enum: ["super_admin", "admin", "organizer", "marketer", "mentor", "judge", "participant", "partner"],
default: "participant",
},
// ... additional fields
},
{ timestamps: true }
);
// Indexes
UserSchema.index({ email: 1 }, { unique: true });
// THE GUARD: Check if model already exists before registering
export const UserModel =
mongoose.models.User || mongoose.model("User", UserSchema);
Pattern 3: Response Utilities
Consistent API responses across all routes.
// src/lib/utils.ts
import { NextResponse } from "next/server";
export function errorResponse(message: string, status: number = 400) {
return NextResponse.json({ error: message }, { status });
}
export function successResponse(data: T, status: number = 200) {
return NextResponse.json(data, { status });
}
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
day: "numeric",
}).format(new Date(date));
}
export function formatDateRange(start: Date, end: Date): string {
return `${formatDate(start)} - ${formatDate(end)}`;
}
Pattern 4: MongoDB Brand Theme
The theme uses MongoDB Brand Book V4.0 tokens. Spring green is accent-only in dark mode — never use it as a background fill.
// src/styles/theme.ts
"use client";
import { createTheme } from "@mui/material/styles";
export const mongoBrand = {
springGreen: "#00ED64", // Accent/CTA only
slateBlue: "#001E2B", // Dark backgrounds, text
white: "#FFFFFF",
forestGreen: "#00684A", // Primary in light mode
evergreen: "#023430", // Primary dark
mist: "#E3FCF7", // Light tint
lavender: "#F9EBFF",
blue: "#006EFF", // Secondary / interactive
purple: "#B45AF2", // Accent
warningYellow: "#FFC010",
errorRed: "#CF4520",
gray: {
50: "#F9FBFA",
100: "#E7EEEC",
200: "#C1CCC6",
300: "#889397",
400: "#5C6C75",
500: "#3D4F58",
600: "#1C2D38",
},
};
export const hackathonTheme = createTheme({
cssVariables: {
colorSchemeSelector: "data-color-scheme",
},
colorSchemes: {
light: {
palette: {
primary: {
main: mongoBrand.forestGreen,
light: mongoBrand.springGreen,
dark: mongoBrand.evergreen,
contrastText: mongoBrand.white,
},
secondary: { main: mongoBrand.blue, light: "#4A90FF", dark: "#0052C2", contrastText: mongoBrand.white },
success: { main: mongoBrand.forestGreen, light: mongoBrand.springGreen, dark: mongoBrand.evergreen },
info: { main: mongoBrand.blue },
warning: { main: mongoBrand.warningYellow, dark: "#E6AC00" },
error: { main: mongoBrand.errorRed, light: "#E8714D", dark: "#A33618" },
background: { default: mongoBrand.white, paper: mongoBrand.white },
text: { primary: mongoBrand.slateBlue, secondary: mongoBrand.gray[400] },
divider: "rgba(0, 30, 43, 0.08)",
},
},
dark: {
palette: {
primary: {
main: mongoBrand.springGreen, // Buttons, icons, links
light: "#33F07F",
dark: "#00C254",
contrastText: mongoBrand.slateBlue,
},
secondary: { main: mongoBrand.blue, light: "#4A90FF", dark: "#0052C2", contrastText: mongoBrand.white },
success: { main: "#00A854", light: "#33C270", dark: mongoBrand.forestGreen, contrastText: mongoBrand.white },
info: { main: mongoBrand.blue },
warning: { main: mongoBrand.warningYellow, dark: "#E6AC00" },
error: { main: "#E8714D", light: "#F09A7D", dark: mongoBrand.errorRed },
background: {
default: mongoBrand.slateBlue, // Page background
paper: "#0F2235", // Card surfaces (dark navy, not green-tinted)
},
text: { primary: "#E8EDEB", secondary: mongoBrand.gray[200] },
divider: "rgba(255, 255, 255, 0.08)",
action: {
hover: "rgba(255, 255, 255, 0.12)",
selected: "rgba(255, 255, 255, 0.20)",
focus: "rgba(255, 255, 255, 0.16)",
},
},
},
},
typography: {
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif",
fontWeightLight: 300,
fontWeightRegular: 400,
fontWeightMedium: 500,
fontWeightBold: 600,
h1: { fontWeight: 600, letterSpacing: "-0.02em", fontSize: "clamp(2rem, 5vw, 3.5rem)", lineHeight: 1.2 },
h2: { fontWeight: 600, letterSpacing: "-0.01em", fontSize: "clamp(1.75rem, 4vw, 3rem)", lineHeight: 1.25 },
h3: { fontWeight: 600, fontSize: "clamp(1.5rem, 3vw, 2.5rem)", lineHeight: 1.3 },
h4: { fontWeight: 600, fontSize: "clamp(1.25rem, 2.5vw, 2rem)", lineHeight: 1.35 },
h5: { fontWeight: 500, fontSize: "clamp(1.125rem, 2vw, 1.5rem)", lineHeight: 1.4 },
h6: { fontWeight: 500, fontSize: "clamp(1rem, 1.5vw, 1.25rem)", lineHeight: 1.45 },
body1: { fontSize: "1rem", lineHeight: 1.6, fontWeight: 400 },
body2: { fontSize: "0.875rem", lineHeight: 1.6, fontWeight: 400 },
button: { fontWeight: 500, letterSpacing: "0.01em", textTransform: "none" },
},
components: {
MuiButton: {
styleOverrides: {
root: { borderRadius: 8, textTransform: "none" as const, fontWeight: 500, padding: "10px 20px" },
contained: { boxShadow: "none", "&:hover": { boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)" } },
outlined: { borderWidth: 1.5, "&:hover": { borderWidth: 1.5 } },
},
},
MuiCard: {
styleOverrides: {
root: ({ theme }) => ({
borderRadius: 12,
boxShadow: "0 1px 3px rgba(0, 0, 0, 0.06), 0 4px 12px rgba(0, 0, 0, 0.04)",
border: `1px solid ${theme.vars.palette.divider}`,
}),
},
},
MuiTextField: {
defaultProps: { variant: "outlined" },
styleOverrides: {
root: { "& .MuiOutlinedInput-root": { borderRadius: 8 } },
},
},
MuiChip: {
styleOverrides: {
root: { borderRadius: 6, fontWeight: 500 },
colorPrimary: ({ theme }) => ({
...theme.applyStyles("dark", {
backgroundColor: "rgba(0, 237, 100, 0.12)",
color: mongoBrand.springGreen,
}),
}),
},
},
MuiTab: {
styleOverrides: {
root: { textTransform: "none" as const, fontWeight: 500, fontSize: "0.95rem" },
},
},
MuiAppBar: {
styleOverrides: {
root: ({ theme }) => ({
backgroundImage: "none",
...theme.applyStyles("dark", {
backgroundColor: mongoBrand.slateBlue,
borderBottom: "1px solid rgba(255, 255, 255, 0.08)",
}),
}),
},
},
},
});
Pattern 5: ThemeRegistry for SSR
Prevents flash-of-wrong-theme and hydration mismatches with Emotion + Next.js App Router.
// src/components/shared-ui/ThemeRegistry.tsx
"use client";
import { useState } from "react";
import { useServerInsertedHTML } from "next/navigation";
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import InitColorSchemeScript from "@mui/material/InitColorSchemeScript";
import { hackathonTheme } from "@/styles/theme";
export default function ThemeRegistry({ children }: { children: React.ReactNode }) {
const [{ cache, flush }] = useState(() => {
const cache = createCache({ key: "mui" });
cache.compat = true;
const prevInsert = cache.insert;
let inserted: string[] = [];
cache.insert = (...args) => {
const serialized = args[1];
if (cache.inserted[serialized.name] === undefined) {
inserted.push(serialized.name);
}
return prevInsert(...args);
};
const flush = () => {
const prevInserted = inserted;
inserted = [];
return prevInserted;
};
return { cache, flush };
});
useServerInsertedHTML(() => {
const names = flush();
if (names.length === 0) return null;
let styles = "";
for (const name of names) {
styles += cache.inserted[name];
}
return (
);
});
return (
{children}
);
}
Pattern 6: Zod Validation Schemas
Always create paired schemas: createXSchema for creation and updateXSchema = createXSchema.partial() for updates.
// src/lib/db/schemas.ts
import { z } from "zod";
export const createEventSchema = z.object({
name: z.string().min(3).max(200),
description: z.string().min(10).max(5000),
startDate: z.string().datetime(),
endDate: z.string().datetime(),
location: z.string().min(2).max(500),
capacity: z.number().int().positive().max(10000),
isVirtual: z.boolean(),
tags: z.array(z.string()).max(20),
});
export const updateEventSchema = createEventSchema.partial();
Pattern 7: Standard API Route
Every API route follows this exact pattern.
// src/app/api/admin/events/route.ts
import { NextRequest } from "next/server";
import { auth } from "@/lib/auth";
import { connectToDatabase } from "@/lib/db/connection";
import { EventModel } from "@/lib/db/models/Event";
import { createEventSchema } from "@/lib/db/schemas";
import { errorResponse, successResponse } from "@/lib/utils";
export async function GET() {
try {
const session = await auth();
if (!session?.user) {
return errorResponse("Unauthorized", 401);
}
await connectToDatabase();
const events = await EventModel.find()
.sort({ startDate: -1 })
.lean();
return successResponse(events);
} catch (error) {
console.error("GET /api/admin/events error:", error);
return errorResponse("Internal server error", 500);
}
}
export async function POST(request: NextRequest) {
try {
const session = await auth();
const role = (session?.user as { role?: string })?.role;
if (!role || !["admin", "super_admin"].includes(role)) {
return errorResponse("Forbidden", 403);
}
const body = await request.json();
const parsed = createEventSchema.safeParse(body);
if (!parsed.success) {
return errorResponse(parsed.error.errors[0].message, 422);
}
await connectToDatabase();
const event = await EventModel.create(parsed.data);
return su
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [mrlynn](https://github.com/mrlynn)
- **Source:** [mrlynn/claude-skills](https://github.com/mrlynn/claude-skills)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.