DEV Community

Abhinav
Abhinav

Posted on

From Zero to Autonomous: How We Built Our AI-Powered Dev.to Content Engine

From Zero to Autonomous: How We Built Our AI-Powered Dev.to Content Engine

Every developer knows the struggle: you've built something incredible, learned a ton, and now you want to share it with the world. But then reality hits. The blank page. The endless rephrasing. The looming deadline for your next blog post. Sound familiar? We've been there, staring at an empty editor, wishing our content could just... write itself. So, we decided to make that wish a reality by building a fully autonomous Dev.to publishing platform.

TL;DR

We built an autonomous Dev.to publishing platform! Our NestJS TypeScript backend uses Google Gemini to generate structured JSON for blog posts and email drafts. A sleek, glassmorphic Vanilla JS frontend provides a crucial human approval workflow before publishing directly to Dev.to via its REST API. This post details our architecture, the integration challenges we overcame, and the strategic choices behind our stack.

The Vision: Automating Content, Not Creativity

Our goal wasn't to replace human writers, but to empower them. We wanted a tool that could rapidly generate high-quality drafts, allowing our team to focus on refining ideas, adding unique perspectives, and ensuring factual accuracy, rather than wrestling with initial drafts and markdown formatting. This led us to envision a platform that handles the repetitive heavy lifting, providing a polished starting point for every piece of content.

Architectural Blueprint: A Symphony of Stacks

Building this system required a thoughtful combination of technologies, each chosen for its specific strengths and how it would contribute to a seamless, robust content pipeline.

Frontend: Vanilla JS & Glassmorphism - Why Go Barebones?

For our internal content management interface, we wanted absolute control over every pixel and interaction without the overhead of a large framework. This led us to choose Vanilla JavaScript.

  • Performance & Control: Pure JS offered unparalleled control and minimal bundle size, crucial for a snappy, responsive internal tool.
  • Custom UI/UX: We designed a custom, glassmorphic interface, giving our approval dashboard a premium, modern feel. The visual appeal of our internal tools was surprisingly important for team adoption.
  • Strict Approval Workflow: This was paramount. The frontend's primary role is to present generated content clearly, allow for easy edits, and facilitate a definitive 'Approve' or 'Reject' decision before anything goes live. It communicates directly with our NestJS backend for content generation and publishing actions.

Backend: NestJS & TypeScript - Our Robust Command Center

For the brains of our operation, we needed something robust, scalable, and developer-friendly. NestJS with TypeScript was a natural fit.

  • Modularity & Scalability: NestJS's modular architecture, inspired by Angular, allowed us to logically separate concerns into distinct modules (e.g., AiModule, DevtoModule, AuthModule). This structure makes the application easy to maintain, test, and scale.
  • Type Safety: TypeScript was crucial. When dealing with AI outputs that should conform to a specific JSON schema, type definitions provide compile-time checks and significantly reduce runtime errors.
  • Dependency Injection: NestJS's powerful DI system simplified component management and testing.
  • API Endpoints: Our backend exposes endpoints for initiating content generation (/ai/generate-post), managing drafts (/posts/:id/save-draft), and handling the final publication process (/posts/:id/publish).

AI Core: Google Gemini API - The Brain of Our Operation

This is where the magic happens. We chose Google Gemini for its multimodal capabilities and excellent fidelity in generating structured JSON output.

  • Structured Output: Our primary use case involved instructing Gemini to produce content strictly in JSON format, adhering to the Dev.to article schema (title, body_markdown, tags, summary). This was a game-changer for automating content processing.
  • Versatility: Beyond blog posts, we leveraged Gemini to draft email newsletters, social media captions, and even internal documentation, all tailored to specific formats and tones.
  • Prompt Engineering: The quality of the output is directly proportional to the quality of the prompt. We spent considerable time refining our prompts, explicitly defining the desired JSON schema and content requirements, including markdown formatting guidelines.

The Glue: Dev.to REST API Integration

Finally, connecting our generated and approved content to its destination: Dev.to. The Dev.to REST API is straightforward and well-documented.

  • Article Management: We utilized endpoints like POST /api/articles to create new posts (initially as drafts) and PUT /api/articles/:id to update them and finally mark them as published.
  • Secure Authentication: API keys were securely managed as environment variables on our NestJS backend, ensuring that our platform could interact with Dev.to on behalf of our team without exposing credentials client-side.

Deep Dive: Key Integrations & Code Snippets

Let's look at some simplified code snippets to illustrate how these components interact.

Gemini Integration Example (Simplified)

Our AiService handles the communication with the Google Gemini API, focusing on requesting structured JSON.

// backend/src/ai/ai.service.ts
import { Injectable } from '@nestjs/common';
import { GoogleGenerativeAI } from '@google/generative-ai';

@Injectable()
export class AiService {
  private readonly genAI: GoogleGenerativeAI;

  constructor() {
    this.genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
  }

  async generateBlogPostContent(topic: string, keywords: string[]): Promise<any> {
    const model = this.genAI.getGenerativeModel({ model: "gemini-pro" });
    const prompt = `Generate a Dev.to blog post in JSON format based on the topic "${topic}" and keywords "${keywords.join(', ')}". The JSON should adhere to the following schema: { "title": "string", "body_markdown": "string", "tags": ["string"], "summary": "string" }. Ensure body_markdown uses clean markdown, including headings, code blocks, and lists. Max 4 tags, alphanumeric only.`;

    try {
      const result = await model.generateContent(prompt);
      const response = await result.response;
      const text = response.text();

      // Gemini often wraps JSON in markdown fences, so we need to extract it
      const jsonMatch = text.match(/```
{% endraw %}
json\n([\s\S]*?)\n
{% raw %}
```/);
      if (jsonMatch && jsonMatch[1]) {
          return JSON.parse(jsonMatch[1]);
      } else {
          // Fallback: try parsing directly if no markdown fences (less common but possible)
          return JSON.parse(text);
      }
    } catch (error) {
      console.error("Failed to parse JSON from Gemini output or API error:", error);
      throw new Error("AI content generation failed.");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Dev.to API Interaction (Simplified)

Our DevtoService orchestrates interaction with the Dev.to REST API.

// backend/src/devto/devto.service.ts
import { Injectable, HttpService } from '@nestjs/common';
import { firstValueFrom } from 'rxjs';

@Injectable()
export class DevtoService {
  constructor(private readonly httpService: HttpService) {}

  async createDevtoArticle(postData: any): Promise<any> {
    const headers = {
      'api-key': process.env.DEVTO_API_KEY,
      'Content-Type': 'application/json'
    };

    const payload = {
      article: {
        title: postData.title,
        body_markdown: postData.body_markdown,
        tags: postData.tags,
        published: false, // Always publish as draft first for approval
        canonical_url: postData.canonical_url, // Optional
        series: postData.series // Optional
      }
    };

    try {
      const response = await firstValueFrom(
        this.httpService.post('https://dev.to/api/articles', payload, { headers }),
      );
      return response.data;
    } catch (error) {
      console.error('Failed to create Dev.to article:', error.response?.data || error.message);
      throw new Error('Dev.to API error: Could not create article.');
    }
  }

  async publishDevtoArticle(articleId: number, articleData: any): Promise<any> {
    const headers = {
      'api-key': process.env.DEVTO_API_KEY,
      'Content-Type': 'application/json'
    };

    const payload = {
      article: {
        ...articleData, // Allow updating title, body, tags, etc. before publishing
        published: true // Mark as published
      }
    };

    try {
      const response = await firstValueFrom(
        this.httpService.put(`https://dev.to/api/articles/${articleId}`, payload, { headers }),
      );
      return response.data;
    } catch (error) {
      console.error('Failed to publish Dev.to article:', error.response?.data || error.message);
      throw new Error('Dev.to API error: Could not publish article.');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Approval Workflow Logic

The full workflow looks something like this:

  1. Request Generation: A user in the frontend submits a topic and keywords to our NestJS backend (/ai/generate-post).
  2. AI Generates Draft: The AiService calls Gemini, receives the JSON, and the backend stores this as a new Post entity in our database (e.g., MongoDB, PostgreSQL) with a status: 'pending_review'.
  3. Review & Edit: The frontend fetches pending_review posts, displaying them in a markdown-rendered preview within our glassmorphic UI. Users can edit the title, body, tags, and summary directly.
  4. Draft to Dev.to: Upon initial generation, the backend can also immediately create a private draft on Dev.to using devtoService.createDevtoArticle({ ...postData, published: false }), storing the article_id in our database.
  5. Approval/Rejection: An authorized user (admin/editor) reviews the post. If approved, the (potentially edited) content is sent to our backend's publishPost endpoint, which then calls devtoService.publishDevtoArticle(articleId, updatedArticleData). If rejected, the internal status is simply updated to rejected, and the Dev.to draft is deleted or left untouched.

Challenges We Faced (And How We Overcame Them)

Building an autonomous system, even with a human in the loop, wasn't without its hurdles.

Prompt Engineering for Consistent JSON

Gemini is powerful, but it needs clear instructions. Initially, we sometimes received malformed JSON or content that didn't strictly adhere to our schema. We overcame this by:

  • Explicit Schema Definition: Including the exact JSON schema in the prompt.
  • Instruction Reinforcement: Adding phrases like "Always respond in JSON format" and "Do not include conversational text outside the JSON block."
  • Output Parsing: Implementing robust regex (/json\n([\s\S]*?)\n/) to reliably extract the JSON string, even when Gemini wrapped it in markdown code fences.

Rate Limiting & Error Handling

Both the Gemini and Dev.to APIs have rate limits. Hitting these could disrupt our workflow.

  • Retries with Exponential Backoff: We implemented a retry mechanism with exponential backoff for API calls, especially for the Gemini API.
  • Graceful Error Reporting: All API errors are caught, logged, and friendly error messages are passed back to the frontend, preventing a broken user experience.
  • Monitoring: We set up basic monitoring for API usage to anticipate and address potential rate limit issues before they become critical.

Security Considerations

Dealing with external APIs and content generation, security was paramount.

  • Environment Variables: All API keys (Gemini, Dev.to) are stored as environment variables and never committed to version control or exposed client-side.
  • Input Validation & Sanitization: Strict validation on all incoming data to our backend, preventing injection attacks and ensuring data integrity.
  • Authentication & Authorization: Our internal frontend required user authentication, and only authorized roles could perform actions like publishing to Dev.to.

UI/UX for Content Review

Making the approval process intuitive was vital. If reviewing was cumbersome, our team wouldn't use it.

  • Real-time Markdown Preview: As users edited the body_markdown, a live preview showed exactly how it would look on Dev.to.
  • Editable Fields: Every part of the generated JSON (title, body, tags, summary) was easily editable.
  • Clear Status Indicators: Visual cues for pending_review, approved, rejected, published states.

Why This Stack? A Strategic Choice

Our technology choices were deliberate, aimed at maximizing efficiency, scalability, and developer experience:

  • NestJS/TypeScript: Provided the robustness, scalability, and type safety needed for a critical backend service. Its enterprise-grade features and strong community made it a confident choice for handling complex logic and external API integrations.
  • Google Gemini: Positioned as a leading AI model, Gemini offered the necessary capabilities for structured content generation with high accuracy. Its continuous improvement aligns with our long-term vision for the platform.
  • Vanilla JS Frontend: For a highly focused internal tool, eschewing a heavy framework meant less boilerplate, faster development, and absolute control over performance and aesthetics. It proved to be a lightweight yet powerful choice for building our custom glassmorphic UI.

Real-world Use Case: Beyond Blog Posts

While this platform was initially designed for Dev.to posts, the core architecture is highly adaptable. We've already started exploring its potential for:

  • Internal Documentation: Generating initial drafts for technical specifications, onboarding guides, or API documentation.
  • Marketing Copy: Quickly producing variations of ad headlines, social media posts, or website copy.
  • Email Newsletters: Drafting segment-specific email content, saving considerable time for our marketing team.
  • Code Snippet Generation: For tutorials or quick examples, AI can provide a useful starting point.

Common Mistakes to Avoid

If you're considering building something similar, learn from our experience:

  • Over-relying on Raw AI Output: Always, always have a human review step. AI is a co-pilot, not a replacement. Unvetted AI content can lead to misinformation or brand damage.
  • Ignoring Approval Workflows: A

Top comments (1)

Collapse
 
abhivyaktii profile image
Abhinav

Disclaimer: This post was generated using AI for a project. If it unintentionally violates any community guidelines, I’m happy to make corrections.