Code Examples

Explore common patterns and best practices for building applications with the NoEgo framework. Each example includes complete, copy-ready code snippets.

Follow this complete workflow to add a new feature from database to UI.

Step 1: Create Migration

Define your database schema changes with up and down migrations.

migrations/001_create_posts.up.sql
CREATE TABLE posts (
  id INT AUTO_INCREMENT PRIMARY KEY,
  title VARCHAR(255) NOT NULL,
  content TEXT NOT NULL,
  author_id INT NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted_at DATETIME,
  FOREIGN KEY (author_id) REFERENCES users(id)
);

CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_posts_deleted ON posts(deleted_at);
migrations/001_create_posts.down.sql
DROP INDEX IF EXISTS idx_posts_deleted ON posts;
DROP INDEX IF EXISTS idx_posts_author ON posts;
DROP TABLE IF EXISTS posts;

Step 2: Create Repository

Use SQLStack with @QueryBinder and co-located SQL files for type-safe database access.

server/repo/posts_repo/index.ts
import { Component } from "@noego/ioc";
import type { WriteResult } from "sqlstack";
import { QueryBinder, Query, Single, SqlStackError } from "sqlstack";

export interface Post {
  id: number;
  title: string;
  content: string;
  author_id: number;
  author_name?: string;
  created_at: string;
  updated_at: string;
  deleted_at: string | null;
}

@QueryBinder()
@Component()
export default class PostsRepo {
  // SQL file: getAllPosts.sql
  @Query()
  getAllPosts(): Promise<Post[]> {
    throw new SqlStackError("Not implemented");
  }

  // SQL file: getPostById.sql
  @Single()
  @Query()
  getPostById(id: number): Promise<Post | null> {
    throw new SqlStackError("Not implemented");
  }

  // SQL file: createPost.sql
  @Query()
  createPost(data: {
    title: string;
    content: string;
    author_id: number;
  }): Promise<WriteResult> {
    throw new SqlStackError("Not implemented");
  }

  // SQL file: updatePost.sql
  @Query()
  updatePost(data: {
    id: number;
    title?: string;
    content?: string;
  }): Promise<WriteResult> {
    throw new SqlStackError("Not implemented");
  }

  // SQL file: deletePost.sql (soft delete)
  @Query()
  deletePost(id: number): Promise<WriteResult> {
    throw new SqlStackError("Not implemented");
  }
}
server/repo/posts_repo/getAllPosts.sql
-- getAllPosts.sql
SELECT
  p.id, p.title, p.content, p.author_id,
  u.name AS author_name,
  p.created_at, p.updated_at
FROM posts p
JOIN users u ON p.author_id = u.id
WHERE p.deleted_at IS NULL
ORDER BY p.created_at DESC;
server/repo/posts_repo/createPost.sql
-- createPost.sql
-- Uses :insert helper for dynamic column insertion
INSERT INTO posts :insert(
  title,
  content,
  author_id,
  created_at = datetime('now'),
  updated_at = datetime('now')
);

Step 3: Create Service

Implement business logic with dependency injection using @Component and @Inject.

server/service/posts_service.ts
import { Component, Inject } from "@noego/ioc";
import PostsRepo from "../repo/posts_repo";

@Component()
export default class PostsService {
  constructor(
    @Inject(PostsRepo) private repo: PostsRepo
  ) {}

  async getAllPosts() {
    return this.repo.getAllPosts();
  }

  async getPostById(id: number) {
    const post = await this.repo.getPostById(id);
    if (!post) {
      throw new Error("Post not found");
    }
    return post;
  }

  async createPost(data: { title: string; content: string; authorId: number }) {
    // Validate input
    if (!data.title || data.title.trim().length < 3) {
      throw new Error("Title must be at least 3 characters");
    }

    const result = await this.repo.createPost({
      title: data.title.trim(),
      content: data.content.trim(),
      author_id: data.authorId
    });

    return Number(result.lastInsertId);
  }

  async updatePost(id: number, data: { title?: string; content?: string }) {
    // Verify post exists
    await this.getPostById(id);

    const result = await this.repo.updatePost({ id, ...data });
    if (result.rowsAffected === 0) {
      throw new Error("Post not found");
    }
  }

  async deletePost(id: number) {
    const result = await this.repo.deletePost(id);
    if (result.rowsAffected === 0) {
      throw new Error("Post not found");
    }
  }
}

Step 4: Create Controller

Handle HTTP requests. Routes are defined in OpenAPI YAML.

server/controller/posts.controller.ts
import { Component, Inject } from "@noego/ioc";
import PostsService from "../service/posts_service";

@Component()
export default class PostsController {
  constructor(
    @Inject(PostsService) private service: PostsService
  ) {}

  async list({ req, res }: any) {
    const posts = await this.service.getAllPosts();
    return { posts };
  }

  async getById({ req, res }: any) {
    const id = Number(req.params.id);
    try {
      const post = await this.service.getPostById(id);
      return post;
    } catch (error) {
      res?.status?.(404);
      return { error: "Post not found" };
    }
  }

  async create({ req, res }: any) {
    const { title, content, authorId } = req.body;
    try {
      const id = await this.service.createPost({ title, content, authorId });
      res?.status?.(201);
      return { id };
    } catch (error) {
      res?.status?.(400);
      return { error: (error as Error).message };
    }
  }

  async update({ req, res }: any) {
    const id = Number(req.params.id);
    const { title, content } = req.body;
    try {
      await this.service.updatePost(id, { title, content });
      res?.status?.(204);
      return;
    } catch (error) {
      res?.status?.(404);
      return { error: "Post not found" };
    }
  }

  async remove({ req, res }: any) {
    const id = Number(req.params.id);
    try {
      await this.service.deletePost(id);
      res?.status?.(204);
      return;
    } catch (error) {
      res?.status?.(404);
      return { error: "Post not found" };
    }
  }
}
server/openapi/posts.yaml
module:
  posts:
    basePath: '/api/posts'
    paths:
      '/':
        get:
          x-controller: posts.controller
          x-action: list
          summary: Get all posts
          responses:
            '200':
              description: List of posts
        post:
          x-controller: posts.controller
          x-action: create
          summary: Create a new post
          requestBody:
            required: true
            content:
              application/json:
                schema:
                  type: object
                  required: [title, content, authorId]
                  properties:
                    title:
                      type: string
                      minLength: 3
                    content:
                      type: string
                    authorId:
                      type: integer
          responses:
            '201':
              description: Post created
      '/{id:\\d+}':
        get:
          x-controller: posts.controller
          x-action: getById
          responses:
            '200':
              description: Post found
            '404':
              description: Post not found
        patch:
          x-controller: posts.controller
          x-action: update
          requestBody:
            content:
              application/json:
                schema:
                  type: object
                  properties:
                    title:
                      type: string
                    content:
                      type: string
          responses:
            '204':
              description: Post updated
        delete:
          x-controller: posts.controller
          x-action: remove
          responses:
            '204':
              description: Post deleted

Step 5: Create Page Loader

Server-side data loading with the .load.ts pattern.

ui/pages/posts/main.load.ts
type RequestData = any;  // Avoids server-only imports

export default async function load(request: RequestData) {
  // Fetch posts from API
  const response = await fetch('/api/posts');

  if (!response.ok) {
    return { posts: [], load_error: 'Failed to load posts' };
  }

  const { posts } = await response.json();
  return { posts, load_error: null };
}

Step 6: Create Svelte Component

Display data in your UI with Svelte 5 runes.

ui/pages/posts/main.svelte
<script lang="ts">
  interface Post {
    id: number;
    title: string;
    content: string;
    author_name: string;
    created_at: string;
  }

  let { posts, load_error } = $props<{
    posts: Post[];
    load_error: string | null;
  }>();
</script>

<div class="max-w-4xl mx-auto py-8 px-4">
  <h1 class="text-3xl font-bold mb-8">Blog Posts</h1>

  {#if load_error}
    <p class="text-red-500">{load_error}</p>
  {:else if posts.length === 0}
    <p class="text-gray-500">No posts yet.</p>
  {:else}
    <div class="space-y-6">
      {#each posts as post}
        <article class="border rounded-lg p-6 hover:shadow-md transition-shadow">
          <h2 class="text-xl font-semibold mb-2">
            <a href="/posts/{post.id}" class="hover:text-orange-600">
              {post.title}
            </a>
          </h2>
          <p class="text-gray-600 mb-4">{post.content.slice(0, 150)}...</p>
          <div class="text-sm text-gray-400">
            By {post.author_name} • {new Date(post.created_at).toLocaleDateString()}
          </div>
        </article>
      {/each}
    </div>
  {/if}
</div>
NoEgo

© 2025 NoEgo. All rights reserved.