npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

curisjs-backend-template

v1.0.0

Published

Production-ready CurisJS backend API template with Todo CRUD example - Runtime Agnostic (Bun, Deno, Node.js)

Readme

CurisJS Backend API Template

A production-ready backend API template built with CurisJS, featuring a complete Todo CRUD application with clean architecture, type safety, and modern best practices.

Runtime Agnostic: Same code runs on Bun, Deno, and Node.js 18+ without any modifications!

✨ Features

  • � Runtime Agnostic: Works on Bun, Deno, Node.js 18+ - write once, run anywhere
  • �🏗️ Clean Architecture: Controller → Service → Repository pattern
  • 🛡️ Type Safety: Full TypeScript with strict mode
  • 📦 Complete CRUD: Todo List API with in-memory database
  • ✅ Validation: Built-in Zod-like schema validation
  • 🔧 Development Ready: Hot reload with tsx watch or native runtime watch
  • 🚀 Production Ready: Error handling, CORS, logging, timing
  • 📊 RESTful API: Proper HTTP methods and status codes
  • 🎯 Query Filters: Pagination, search, and filtering support
  • 🔌 DB Ready: Prepared for @curisjs/db integration

📋 Prerequisites

Choose your runtime (all supported!):

🚀 Quick Start

Using Bun (Recommended)

# Install dependencies
bun install

# Development
bun run dev:bun

# Production
bun run build
bun run start:bun

Using Deno

# No install needed! Run directly
deno run --allow-net --allow-read --allow-env --watch src/index.ts

# Or use the npm script
bun install  # Only for TypeScript definitions
bun run dev:deno

Using Node.js 18+

# Install dependencies
npm install
# or
pnpm install

# Development
npm run dev

# Production
npm run build
npm start

2. Setup Environment (Optional)

cp .env.example .env

Edit .env if needed:

NODE_ENV=development
APP_DEBUG=true
PORT=3000
DATABASE_PATH=./database.sqlite

3. Start Development Server

pnpm dev

The API will be available at http://localhost:3000

📚 API Endpoints

Health Check

GET /health

Response:

{
  "success": true,
  "message": "API is running",
  "timestamp": "2024-01-01T00:00:00.000Z"
}

List Todos

GET /todos?completed=false&search=buy&limit=20&offset=0

Query Parameters:

  • completed (boolean, optional): Filter by completion status
  • search (string, optional): Search in title and description
  • limit (number, optional): Items per page (default: 20, max: 100)
  • offset (number, optional): Pagination offset (default: 0)

Response:

{
  "success": true,
  "data": [
    {
      "id": 1,
      "title": "Buy groceries",
      "description": "Milk, bread, eggs",
      "completed": false,
      "created_at": "2024-01-01T00:00:00.000Z",
      "updated_at": "2024-01-01T00:00:00.000Z"
    }
  ],
  "pagination": {
    "total": 1,
    "limit": 20,
    "offset": 0,
    "hasMore": false
  }
}

Get Single Todo

GET /todos/:id

Response:

{
  "success": true,
  "data": {
    "id": 1,
    "title": "Buy groceries",
    "description": "Milk, bread, eggs",
    "completed": false,
    "created_at": "2024-01-01T00:00:00.000Z",
    "updated_at": "2024-01-01T00:00:00.000Z"
  }
}

Create Todo

POST /todos
Content-Type: application/json

{
  "title": "Buy groceries",
  "description": "Milk, bread, eggs",
  "completed": false
}

Validation Rules:

  • title: Required, 1-200 characters
  • description: Optional, max 1000 characters
  • completed: Optional, boolean (default: false)

Response (201):

{
  "success": true,
  "data": {
    "id": 1,
    "title": "Buy groceries",
    "description": "Milk, bread, eggs",
    "completed": false,
    "created_at": "2024-01-01T00:00:00.000Z",
    "updated_at": "2024-01-01T00:00:00.000Z"
  },
  "message": "Todo created successfully"
}

Update Todo

PUT /todos/:id
Content-Type: application/json

{
  "title": "Buy groceries and snacks",
  "completed": true
}

Note: All fields are optional in updates

Response:

{
  "success": true,
  "data": {
    "id": 1,
    "title": "Buy groceries and snacks",
    "description": "Milk, bread, eggs",
    "completed": true,
    "created_at": "2024-01-01T00:00:00.000Z",
    "updated_at": "2024-01-01T00:10:00.000Z"
  },
  "message": "Todo updated successfully"
}

Toggle Todo Status

PATCH /todos/:id/toggle

Response:

{
  "success": true,
  "data": {
    "id": 1,
    "title": "Buy groceries",
    "completed": true,
    "created_at": "2024-01-01T00:00:00.000Z",
    "updated_at": "2024-01-01T00:10:00.000Z"
  },
  "message": "Todo status toggled successfully"
}

Delete Todo

DELETE /todos/:id

Response:

{
  "success": true,
  "message": "Todo deleted successfully"
}

📁 Project Structure

src/
├── app/
│   ├── controllers/       # HTTP request handlers
│   │   └── TodoController.ts
│   ├── services/          # Business logic layer
│   │   └── TodoService.ts
│   ├── repositories/      # Data access layer
│   │   └── TodoRepository.ts
│   ├── validators/        # Input validation schemas
│   │   └── TodoValidator.ts
│   └── models/           # TypeScript interfaces
│       └── Todo.ts
├── database/             # Database setup & connection
│   └── connection.ts
├── middleware/           # Custom middleware
│   ├── errorHandler.ts
│   └── timing.ts
├── routes/              # Route definitions
│   ├── index.ts
│   └── todos.ts
└── index.ts            # Application entry point

🏗️ Architecture

Layered Architecture

The template follows a clean, layered architecture:

Controller → Service → Repository → Database

Benefits:

  • Separation of Concerns: Each layer has a single responsibility
  • Testability: Easy to mock and test each layer independently
  • Maintainability: Changes in one layer don't affect others
  • Reusability: Business logic can be shared across controllers

Layer Responsibilities

  1. Controller: Handle HTTP requests/responses, validation, formatting
  2. Service: Business logic, orchestration, transactions
  3. Repository: Data access, database queries
  4. Models: Type definitions, interfaces

🛠️ Development

Available Scripts

# Development with hot reload
pnpm dev

# Build for production
pnpm build

# Start production server
pnpm start

# Type checking
pnpm typecheck

# Linting
pnpm lint

# Format code
pnpm format

# Clean build directory
pnpm clean

Adding a New Feature

Let's add a "User" feature as an example:

1. Create Model

// src/app/models/User.ts
export interface User {
  id: number;
  name: string;
  email: string;
  created_at: string;
}

export interface CreateUserInput {
  name: string;
  email: string;
}

2. Create Validator

// src/app/validators/UserValidator.ts
import { z } from '@curisjs/core';

export const createUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
});

3. Create Repository

// src/app/repositories/UserRepository.ts
import { db } from '../../database/connection.js';
import type { User, CreateUserInput } from '../models/User.js';

export class UserRepository {
  findAll(): User[] {
    return db.prepare('SELECT * FROM users').all() as User[];
  }

  findById(id: number): User | null {
    return db.prepare('SELECT * FROM users WHERE id = ?').get(id) as User | null;
  }

  create(input: CreateUserInput): User {
    const result = db
      .prepare('INSERT INTO users (name, email) VALUES (?, ?)')
      .run(input.name, input.email);

    return this.findById(result.lastInsertRowid as number)!;
  }
}

4. Create Service

// src/app/services/UserService.ts
import { UserRepository } from '../repositories/UserRepository.js';
import type { User, CreateUserInput } from '../models/User.js';

export class UserService {
  private repository = new UserRepository();

  async findAll(): Promise<User[]> {
    return this.repository.findAll();
  }

  async create(input: CreateUserInput): Promise<User> {
    // Add business logic here (e.g., check for duplicate email)
    return this.repository.create(input);
  }
}

5. Create Controller

// src/app/controllers/UserController.ts
import { json } from '@curisjs/core';
import type { Context } from '@curisjs/core';
import { UserService } from '../services/UserService.js';
import { createUserSchema } from '../validators/UserValidator.js';

export class UserController {
  private service = new UserService();

  async index(ctx: Context): Promise<Response> {
    const users = await this.service.findAll();
    return json({ success: true, data: users });
  }

  async create(ctx: Context): Promise<Response> {
    const result = await ctx.validate(createUserSchema);

    if (!result.success) {
      return json(
        {
          success: false,
          error: 'Validation failed',
          details: result.error.format(),
        },
        { status: 400 }
      );
    }

    const user = await this.service.create(result.data);
    return json({ success: true, data: user }, { status: 201 });
  }
}

6. Register Routes

// src/routes/users.ts
import type { App } from '@curisjs/core';
import { UserController } from '../app/controllers/UserController.js';

export function registerUserRoutes(app: App): void {
  const controller = new UserController();

  app.get('/users', (ctx) => controller.index(ctx));
  app.post('/users', (ctx) => controller.create(ctx));
}
// src/routes/index.ts
import { registerUserRoutes } from './users.js';

export function registerRoutes(app: App): void {
  // ... existing routes
  registerUserRoutes(app);
}

🗄️ Database

SQLite Setup

The template uses better-sqlite3 for synchronous SQLite operations:

  • Fast and efficient
  • Type-safe with TypeScript
  • No async/await overhead for simple queries
  • Perfect for small to medium applications

Migrations

For production apps, consider adding a migration system. Here's a simple approach:

// src/database/migrations/001_create_users.ts
export function up(db: Database) {
  db.exec(`
    CREATE TABLE users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL,
      email TEXT UNIQUE NOT NULL,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
  `);
}

export function down(db: Database) {
  db.exec('DROP TABLE users');
}

Switching to PostgreSQL

To use PostgreSQL instead of SQLite:

  1. Install dependencies:
pnpm add pg
pnpm add -D @types/pg
  1. Update repository to use pg:
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

// Use async queries
const result = await pool.query('SELECT * FROM todos');

🧪 Testing

Add tests using Vitest:

pnpm add -D vitest @vitest/ui

Example test:

// src/app/services/TodoService.test.ts
import { describe, it, expect } from 'vitest';
import { TodoService } from './TodoService';

describe('TodoService', () => {
  it('should create a todo', async () => {
    const service = new TodoService();
    const todo = await service.create({
      title: 'Test todo',
      completed: false,
    });

    expect(todo).toHaveProperty('id');
    expect(todo.title).toBe('Test todo');
  });
});

🚀 Production Deployment

Build for Production

pnpm build

Environment Variables

Set these in production:

NODE_ENV=production
APP_DEBUG=false
PORT=3000
DATABASE_PATH=/var/lib/app/database.sqlite

Docker Support

Create Dockerfile:

FROM node:18-alpine

WORKDIR /app

COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile

COPY . .
RUN pnpm build

EXPOSE 3000

CMD ["node", "dist/index.js"]

Build and run:

docker build -t curis-backend .
docker run -p 3000:3000 curis-backend

🔧 Configuration

CORS Configuration

Modify CORS settings in src/index.ts:

app.use(
  cors({
    origin: ['https://yourdomain.com', 'https://app.yourdomain.com'],
    methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
    credentials: true,
    allowedHeaders: ['Content-Type', 'Authorization'],
  })
);

Custom Middleware

Add middleware in src/middleware/:

// src/middleware/auth.ts
import type { Middleware } from '@curisjs/core';

export function auth(): Middleware {
  return async (ctx, next) => {
    const token = ctx.header('Authorization');

    if (!token) {
      return new Response('Unauthorized', { status: 401 });
    }

    // Verify token...
    await next();
  };
}

Use in routes:

import { auth } from '../middleware/auth.js';

app.use(auth());

📖 Learn More

📄 License

MIT

🤝 Contributing

Contributions are welcome! Please open an issue or submit a pull request.

💬 Support