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 🙏

© 2026 – Pkg Stats / Ryan Hefner

apiform

v0.4.0

Published

Auto-generate CRUD routes from your Prisma schema

Downloads

487

Readme

apiform

Auto-generate CRUD REST API routes from your Prisma schema — with consistent responses, pagination, search, and full customization.

npm version License: MIT


What is apiform?

apiform sits on top of your existing Prisma setup and automatically generates a fully structured REST API from your models. No writing controllers. No repetitive route handlers. Just plug in your Prisma client and your API is ready.

Your Prisma Schema → apiform → Fully structured REST API

Every route returns a consistent, predictable response shape — making your API easier to consume and debug.


Features

  • Auto-generated CRUD routes from your Prisma schema
  • Consistent response shape across all endpoints
  • Built-in pagination, search, sorting, and filtering on all GET list endpoints
  • Soft delete support — automatic soft delete for models with deletedAt field
  • Nested relations — include related models via ?include= query parameter
  • TypeScript generics — fully typed responses out of the box
  • Role-Based Access Control (RBAC) — global and per-route role protection
  • Rate limiting — global and per-route rate limiting out of the box
  • Fully customizable — disable routes, add middleware, change prefixes per model
  • Custom routes — add your own routes on top of generated ones
  • TypeScript first — full type safety and intellisense out of the box
  • Fastify powered — fast and lightweight HTTP layer

Requirements

  • Node.js >= 20
  • Prisma >= 7.0.0
  • TypeScript >= 5.0.0

Installation

npm install apiform
# or
bun add apiform

Quick Start

1. Set up your Prisma client (Prisma v7 requires an adapter):

import { PrismaClient } from "@prisma/client";
import { PrismaLibSql } from "@prisma/adapter-libsql";

const adapter = new PrismaLibSql({ url: "file:./prisma/dev.db" });
const prisma = new PrismaClient({ adapter });

2. Pass it to ApiForm and start the server:

import { ApiForm } from "apiform";

const app = new ApiForm(prisma, {
  globalPrefix: "/api",
  models: {
    user: true,
    post: true,
  },
});

app.start(3000);

That's it. Your API is running with the following routes auto-generated for each model:

| Method | Route | Action | | ------ | ---------------- | -------------------- | | GET | /api/users | Find all (paginated) | | GET | /api/users/:id | Find by ID | | POST | /api/users | Create | | PATCH | /api/users/:id | Update | | DELETE | /api/users/:id | Delete |


Response Shape

Every endpoint returns the same consistent structure:

Success (single record):

{
  "success": true,
  "message": "USER_CREATED_SUCCESSFULLY",
  "data": {},
  "meta": null,
  "error": null
}

Success (list):

{
  "success": true,
  "message": "USERS_RETRIEVED_SUCCESSFULLY",
  "data": [],
  "meta": {
    "total": 100,
    "page": 1,
    "limit": 10,
    "totalPages": 10,
    "hasNext": true,
    "hasPrev": false
  },
  "error": null
}

Error:

{
  "success": false,
  "message": "VALIDATION_ERROR",
  "data": null,
  "meta": null,
  "error": {
    "code": "VALIDATION_ERROR",
    "details": []
  }
}

Pagination, Search & Filtering

All GET list endpoints support the following query parameters out of the box:

| Parameter | Type | Description | | ------------- | --------------- | ------------------------------------- | | page | number | Page number (default: 1) | | limit | number | Items per page (default: 10) | | searchBy | string | Field name to search on | | searchValue | string | Value to search for | | sortBy | string | Field to sort by (default: createdAt) | | sortOrder | asc or desc | Sort direction (default: desc) | | filters | JSON string | Additional field filters |

Example:

GET /api/users?page=2&limit=5&searchBy=name&searchValue=john&sortBy=createdAt&sortOrder=desc

Customization

Disable a specific route

const app = new ApiForm(prisma, {
  models: {
    user: {
      delete: { enabled: false },
    },
  },
});

Add middleware to a route

const app = new ApiForm(prisma, {
  models: {
    user: {
      findAll: {
        middleware: [authMiddleware],
      },
    },
  },
});

Custom route prefix per model

const app = new ApiForm(prisma, {
  models: {
    user: {
      prefix: "/members",
    },
  },
});
// Routes: GET /api/members, POST /api/members, etc.

Global middleware

const app = new ApiForm(prisma, {
  globalMiddleware: [loggingMiddleware, authMiddleware],
  models: {
    user: true,
  },
});

Disable an entire model

const app = new ApiForm(prisma, {
  models: {
    user: true,
    post: false, // no routes generated for Post
  },
});

Add custom routes

Use addRoutes() to add your own routes on top of the auto-generated ones. Custom routes are always registered after apiform's routes, so they won't be overwritten.

const app = new ApiForm(prisma, {
  globalPrefix: "/api",
  models: { user: true },
});

app.addRoutes((fastify) => {
  fastify.get("/api/users/count", async (request, reply) => {
    const count = await prisma.user.count();
    reply.send({
      success: true,
      message: "USERS_COUNTED_SUCCESSFULLY",
      data: { count },
      meta: null,
      error: null,
    });
  });
});

app.start(3000);

addRoutes() is chainable — you can call it multiple times:

app.addRoutes(userRoutes).addRoutes(postRoutes).start(3000);

Override a generated route

To override one of apiform's auto-generated routes, simply register the same method and path inside addRoutes(). Since custom routes are registered after apiform's routes, yours will take precedence:

app.addRoutes((fastify) => {
  // Overrides apiform's default GET /api/users
  fastify.get("/api/users", async (request, reply) => {
    // your custom implementation
  });
});

Soft Delete

Models with a deletedAt DateTime? field automatically use soft delete — records are never permanently deleted, just marked with a timestamp.

Add deletedAt to your Prisma model:

model User {
  id        Int       @id @default(autoincrement())
  name      String
  email     String    @unique
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?
}

Auto-generated soft delete routes:

| Method | Route | Action | | ------ | ------------------------ | ----------------------------- | | DELETE | /api/users/:id | Soft delete (sets deletedAt) | | GET | /api/users/deleted | Find all soft deleted records | | PATCH | /api/users/:id/restore | Restore a soft deleted record |

Soft deleted records are automatically excluded from all GET list and findById queries.

Enable soft delete routes in config:

const app = new ApiForm(prisma, {
  models: {
    user: {
      findDeleted: { enabled: true },
      restore: { enabled: true },
    },
  },
});

Custom soft delete field name:

const app = new ApiForm(prisma, {
  models: {
    user: {
      softDelete: "deleted_at", // use custom field name
    },
  },
});

⚠️ Soft Delete & Relations

apiform does not prevent linking records to soft deleted related records. It is the developer's responsibility to ensure relation integrity via custom middleware, application-level validation, or Prisma's referential actions.


Role-Based Access Control (RBAC)

Protect your auto-generated routes with role-based access control. apiform checks the user's roles from the request and returns 403 FORBIDDEN if they don't have the required role.

Setup:

const app = new ApiForm(prisma, {
  rbac: {
    rolesPath: "user.roles", // where to find roles on the request (default: "user.roles")
    globalRoles: ["user"], // roles required for ALL routes
  },
  models: {
    user: {
      findAll: { roles: ["admin"] }, // override — only admin can list users
      delete: { roles: ["admin"] }, // override — only admin can delete
    },
  },
});

How it works:

  • globalRoles applies to every route unless overridden
  • Per-route roles overrides globalRoles for that specific route
  • If no roles are configured, the route is public
  • Roles are looked up from the request using rolesPath (supports dot notation e.g. auth.user.roles)

Custom roles path:

rbac: {
  rolesPath: "auth.roles",  // looks at request.auth.roles
}

Response when access is denied:

{
  "success": false,
  "message": "You do not have permission to access this resource",
  "data": null,
  "meta": null,
  "error": {
    "code": "FORBIDDEN"
  }
}

Note: apiform does not handle authentication — it only checks roles. You are responsible for populating request.user (or your custom path) via your own auth middleware before apiform's RBAC runs.


TypeScript Generics

All CRUD operations support TypeScript generics for fully typed responses:

import type { User } from "@prisma/client";

const result = await crud.findById<User>("user", 1);
result.data.email; // ✅ typed as string
result.data.name; // ✅ typed as string

const list = await crud.findAll<User>("user", {});
list.data; // ✅ typed as User[]

Nested Relations

Include related models in your queries using the ?include= query parameter:

GET /api/posts?include=author
GET /api/users/1?include=posts
GET /api/posts?include=author,comments

Example response with included relation:

{
  "success": true,
  "message": "POSTS_RETRIEVED_SUCCESSFULLY",
  "data": [
    {
      "id": 1,
      "title": "Hello World",
      "author": {
        "id": 1,
        "name": "John Doe"
      }
    }
  ],
  "meta": { ... },
  "error": null
}

Rate Limiting

Protect your API from abuse with built-in rate limiting powered by @fastify/rate-limit.

Global rate limit:

const app = new ApiForm(prisma, {
  rateLimit: {
    max: 100, // maximum requests
    timeWindow: 60, // per 60 seconds
  },
  models: {
    user: true,
  },
});

Per route override:

const app = new ApiForm(prisma, {
  rateLimit: {
    max: 100,
    timeWindow: 60,
  },
  models: {
    user: {
      create: { rateLimit: { max: 10, timeWindow: 60 } }, // stricter on create
    },
  },
});

Response when rate limit is exceeded:

{
  "success": false,
  "message": "RATE_LIMIT_EXCEEDED",
  "data": null,
  "meta": null,
  "error": {
    "code": "TOO_MANY_REQUESTS"
  }
}

Rate limit headers are automatically included in every response:

  • x-ratelimit-limit — maximum requests allowed
  • x-ratelimit-remaining — requests remaining in current window
  • x-ratelimit-reset — seconds until the window resets

Configuration Reference

new ApiForm(prismaClient, {
  globalPrefix?: string;          // default: "/api"
  globalMiddleware?: Function[];   // runs before every route
  schemaPath?: string;            // custom path to schema.prisma
  rateLimit?: {
    max: number;                  // maximum requests
    timeWindow: number;           // time window in seconds
  };
  rbac?: {
    rolesPath?: string;           // default: "user.roles"
    globalRoles?: string[];       // roles required for all routes
  };
  models?: {
    [modelName]: boolean | {
      prefix?: string;
      softDelete?: boolean | string;
      create?: RouteOptions;
      findAll?: RouteOptions;
      findById?: RouteOptions;
      update?: RouteOptions;
      delete?: RouteOptions;
      restore?: RouteOptions;
      findDeleted?: RouteOptions;
    }
  }
});

// RouteOptions
{
  enabled?: boolean;        // default: true
  middleware?: Function[];  // route-level middleware
  roles?: string[];         // roles required for this route
  rateLimit?: {
    max: number;            // override global rate limit
    timeWindow: number;     // time window in seconds
  };
}

Error Codes

| Code | Description | | ------------------ | ------------------------------ | | VALIDATION_ERROR | Request body failed validation | | NOT_FOUND | Record not found | | CONFLICT | Unique constraint violation | | BAD_REQUEST | Invalid request | | INTERNAL_ERROR | Unexpected server error | | UNAUTHORIZED | Unauthorized access | | FORBIDDEN | Forbidden access |


License

MIT © Bibek Raj Ghimire