
Create articles from any YouTube video or use our API to get YouTube transcriptions
Start for freeIn this comprehensive tutorial, we'll walk through building a full-stack code execution platform using Next.js, Convex, and Lemon Squeezy. This platform allows users to write, execute, and share code snippets in multiple programming languages. We'll cover everything from setting up the project to implementing key features and deploying the final application.
Project Setup
Let's start by setting up our Next.js project:
npx [email protected] .
We're using a specific version (15.0.3) to ensure compatibility throughout this tutorial.
Authentication with Clerk
For authentication, we'll use Clerk. First, install the necessary packages:
npm install @clerk/nextjs
Then, set up your Clerk application and add the required environment variables to your .env.local
file:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key
CLERK_SECRET_KEY=your_secret_key
Create a middleware file to handle authentication:
// middleware.ts
import { authMiddleware } from "@clerk/nextjs";
export default authMiddleware();
export const config = {
matcher: ["/((?!.*\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
Wrap your application with the ClerkProvider in your layout.tsx
file:
import { ClerkProvider } from '@clerk/nextjs';
export default function RootLayout({ children }) {
return (
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
);
}
Setting up Convex
Convex will serve as our backend database and real-time data layer. Install Convex:
npm install convex
Initialize Convex in your project:
npx convex dev
This will create a convex
folder and add necessary environment variables to your .env.local
file.
Create a schema file in the convex
folder to define your database tables:
// convex/schema.ts
import { defineSchema, defineTable } from "convex/schema";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
userId: v.string(),
email: v.string(),
name: v.string(),
isPro: v.boolean(),
proSince: v.optional(v.number()),
lemonSqueezyCustomerId: v.optional(v.string()),
lemonSqueezyOrderId: v.optional(v.string()),
}).index("by_userId", ["userId"]),
codeExecutions: defineTable({
userId: v.string(),
language: v.string(),
code: v.string(),
output: v.optional(v.string()),
error: v.optional(v.string()),
}).index("by_userId", ["userId"]),
snippets: defineTable({
userId: v.string(),
username: v.string(),
title: v.string(),
language: v.string(),
code: v.string(),
}).index("by_userId", ["userId"]),
comments: defineTable({
snippetId: v.id("snippets"),
userId: v.string(),
username: v.string(),
content: v.string(),
}).index("by_snippetId", ["snippetId"]),
stars: defineTable({
userId: v.string(),
snippetId: v.id("snippets"),
})
.index("by_userId", ["userId"])
.index("by_snippetId", ["snippetId"])
.index("by_userId_and_snippetId", ["userId", "snippetId"]),
});
Building the Code Editor
For our code editor, we'll use the Monaco Editor. Install the necessary packages:
npm install @monaco-editor/react
Create a custom hook to manage the editor state:
// hooks/useCodeEditor.ts
import { useState, useEffect } from 'react';
import { languageConfig } from '../constants';
export const useCodeEditor = () => {
const [language, setLanguage] = useState('javascript');
const [code, setCode] = useState('');
const [output, setOutput] = useState('');
const [isRunning, setIsRunning] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const savedCode = localStorage.getItem(`editorCode_${language}`);
if (savedCode) {
setCode(savedCode);
} else {
setCode(languageConfig[language].defaultCode);
}
}, [language]);
const handleCodeChange = (newCode) => {
setCode(newCode);
localStorage.setItem(`editorCode_${language}`, newCode);
};
const handleLanguageChange = (newLanguage) => {
setLanguage(newLanguage);
};
const runCode = async () => {
setIsRunning(true);
setError(null);
setOutput('');
try {
// Implement code execution logic here
} catch (err) {
setError(err.message);
} finally {
setIsRunning(false);
}
};
return {
language,
code,
output,
isRunning,
error,
setLanguage: handleLanguageChange,
setCode: handleCodeChange,
runCode,
};
};
Create the EditorPanel component:
// components/EditorPanel.tsx
import { useCodeEditor } from '../hooks/useCodeEditor';
import { Editor } from '@monaco-editor/react';
export const EditorPanel = () => {
const { language, code, setCode } = useCodeEditor();
return (
<div className="relative h-full">
<Editor
height="100%"
language={language}
value={code}
onChange={setCode}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 14,
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</div>
);
};
Implementing Code Execution
To execute code, we'll use the Piston API. Create a new file to handle code execution:
// lib/codeExecution.ts
import { languageConfig } from '../constants';
const PISTON_API_URL = 'https://emkc.org/api/v2/piston/execute';
export const executeCode = async (language, code) => {
const runtime = languageConfig[language].pistonRuntime;
const response = await fetch(PISTON_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
language: runtime.language,
version: runtime.version,
files: [{ content: code }],
}),
});
const data = await response.json();
if (data.message) {
throw new Error(data.message);
}
if (data.compile && data.compile.code !== 0) {
throw new Error(data.compile.stderr || data.compile.output);
}
if (data.run.code !== 0) {
throw new Error(data.run.stderr || data.run.output);
}
return data.run.output.trim();
};
Update the runCode
function in the useCodeEditor
hook to use this execution function:
const runCode = async () => {
setIsRunning(true);
setError(null);
setOutput('');
try {
const result = await executeCode(language, code);
setOutput(result);
} catch (err) {
setError(err.message);
} finally {
setIsRunning(false);
}
};
Building the Snippets Feature
Create a new file for snippet-related functions:
// convex/snippets.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
export const createSnippet = mutation({
args: {
title: v.string(),
language: v.string(),
code: v.string(),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Not authenticated");
}
const user = await ctx.db
.query("users")
.filter((q) => q.eq(q.field("userId"), identity.subject))
.first();
if (!user) {
throw new Error("User not found");
}
const snippet = await ctx.db.insert("snippets", {
userId: identity.subject,
username: user.name,
title: args.title,
language: args.language,
code: args.code,
});
return snippet;
},
});
export const getSnippets = query({
handler: async (ctx) => {
return await ctx.db.query("snippets").order("desc").collect();
},
});
export const getSnippetById = query({
args: { id: v.id("snippets") },
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
},
});
Create a SnippetCard component to display snippets:
// components/SnippetCard.tsx
import Link from 'next/link';
import { formatDistanceToNow } from 'date-fns';
import { StarButton } from './StarButton';
export const SnippetCard = ({ snippet }) => {
return (
<div className="bg-gray-800 rounded-lg p-4 shadow-md">
<div className="flex justify-between items-center mb-2">
<h3 className="text-lg font-semibold text-white">{snippet.title}</h3>
<StarButton snippetId={snippet._id} />
</div>
<p className="text-gray-400 text-sm mb-2">
by {snippet.username} • {formatDistanceToNow(new Date(snippet._creationTime))} ago
</p>
<div className="bg-gray-900 rounded p-2 mb-2">
<code className="text-sm text-gray-300">{snippet.code.slice(0, 100)}...</code>
</div>
<Link href={`/snippets/${snippet._id}`} className="text-blue-400 hover:text-blue-300 text-sm">
View full snippet
</Link>
</div>
);
};
Implementing the Star Feature
Create functions for starring and unstarring snippets:
// convex/snippets.ts
export const starSnippet = mutation({
args: { snippetId: v.id("snippets") },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Not authenticated");
}
const existingStar = await ctx.db
.query("stars")
.filter((q) =>
q.and(
q.eq(q.field("userId"), identity.subject),
q.eq(q.field("snippetId"), args.snippetId)
)
)
.first();
if (existingStar) {
await ctx.db.delete(existingStar._id);
} else {
await ctx.db.insert("stars", {
userId: identity.subject,
snippetId: args.snippetId,
});
}
},
});
export const getStarCount = query({
args: { snippetId: v.id("snippets") },
handler: async (ctx, args) => {
const stars = await ctx.db
.query("stars")
.filter((q) => q.eq(q.field("snippetId"), args.snippetId))
.collect();
return stars.length;
},
});
Create a StarButton component:
// components/StarButton.tsx
import { useState } from 'react';
import { useMutation, useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
export const StarButton = ({ snippetId }) => {
const starCount = useQuery(api.snippets.getStarCount, { snippetId });
const star = useMutation(api.snippets.starSnippet);
const [isStarred, setIsStarred] = useState(false);
const handleStar = async () => {
await star({ snippetId });
setIsStarred(!isStarred);
};
return (
<button
onClick={handleStar}
className={`flex items-center space-x-1 ${isStarred ? 'text-yellow-400' : 'text-gray-400'}`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
<span>{starCount}</span>
</button>
);
};
Setting up Lemon Squeezy for Payments
To handle payments, we'll use Lemon Squeezy. First, create an account and set up a product for your Pro plan.
Install the Lemon Squeezy SDK:
npm install lemonsqueezy.ts
Create a new file to handle Lemon Squeezy interactions:
// lib/lemonSqueezy.ts
import { LemonSqueezy } from 'lemonsqueezy.ts';
const ls = new LemonSqueezy(process.env.LEMON_SQUEEZY_API_KEY);
export const createCheckout = async (userEmail) => {
const checkout = await ls.createCheckout({
storeId: process.env.LEMON_SQUEEZY_STORE_ID,
variantId: process.env.LEMON_SQUEEZY_VARIANT_ID,
customData: { userEmail },
});
return checkout.data.attributes.url;
};
Create a webhook handler to process successful payments:
// pages/api/lemon-squeezy-webhook.js
import { buffer } from 'micro';
import { verifyWebhook } from 'lemonsqueezy.ts';
import { updateUserToPro } from '../../lib/users';
export const config = {
api: {
bodyParser: false,
},
};
export default async function handler(req, res) {
if (req.method === 'POST') {
const buf = await buffer(req);
const signature = req.headers['x-signature'];
try {
const event = verifyWebhook(buf, signature, process.env.LEMON_SQUEEZY_WEBHOOK_SECRET);
if (event.type === 'order_created') {
const { user_email } = event.data.attributes.custom_data;
await updateUserToPro(user_email);
}
res.status(200).json({ message: 'Webhook processed successfully' });
} catch (err) {
console.error('Webhook error:', err);
res.status(400).json({ message: 'Webhook verification failed' });
}
} else {
res.setHeader('Allow', 'POST');
res.status(405).end('Method Not Allowed');
}
}
Update the user's status to Pro:
// lib/users.ts
import { api } from '../convex/_generated/api';
export const updateUserToPro = async (email) => {
await api.users.updateToPro({ email });
};
Building the Profile Page
Create a profile page to display user information and their snippets:
// pages/profile.tsx
import { useUser } from '@clerk/nextjs';
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
import { SnippetCard } from '../components/SnippetCard';
export default function ProfilePage() {
const { user } = useUser();
const snippets = useQuery(api.snippets.getUserSnippets, { userId: user?.id });
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">Your Profile</h1>
<div className="mb-8">
<h2 className="text-2xl font-semibold mb-4">Your Snippets</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{snippets?.map((snippet) => (
<SnippetCard key={snippet._id} snippet={snippet} />
))}
</div>
</div>
</div>
);
}
Deploying the Application
To deploy your application, you can use Vercel. First, push your code to a GitHub repository.
Then, follow these steps:
- Sign up for a Vercel account and connect it to your GitHub account.
- Create a new project and select your repository.
- Configure your environment variables in the Vercel dashboard.
- Deploy your application.
Make sure to update your Clerk and Convex configurations with the production URL of your deployed application.
Conclusion
In this tutorial, we've built a full-stack code execution platform using Next.js, Convex, and Lemon Squeezy. We've implemented key features such as code execution, snippet sharing, and a Pro plan with payments. This application demonstrates how to combine modern web technologies to create a powerful and scalable platform.
To further improve the application, consider adding features like:
- User profiles with avatars
- Commenting on snippets
- Tagging snippets for better organization
- Advanced code editor features (e.g., multiple files, themes)
- Integration with version control systems
Remember to always follow best practices for security, performance, and user experience as you continue to develop and maintain your application.
Article created from: https://youtu.be/fGkRQgf6Scw?si=DHiRt7SWZNkcf2hP