S
Get Started

Project Structure

A recommended folder layout for a StarkForge backend and what belongs in each file

StarkForge does not scan your folders. It knows only the collections you import and pass to new Forge, so the layout of your project is your choice. This page recommends one that stays readable as the API grows: one file per collection, one file that builds the configuration and one small entry file.

  • acme-api
  • src
  • collections
  • products.ts
  • config.ts
  • server.ts
  • tests
  • products.test.ts
  • .env
  • .env.example
  • .gitignore
  • package.json
  • tsconfig.json
PathWhat it holds
src/collections/One file per collection. Each file exports one collection() call: the route, the model, the schema, the validation groups and the routes of one resource
src/config.tscreateOptions(), which reads process.env and returns the options for new Forge, including the list of collections
src/server.tsThe entry point. It creates the Forge and starts it
tests/Tests. They build the same app from createOptions() and a fake database
.envThe real values for your machine. Never commit it
.env.exampleThe same names with fake values, committed so that other developers know what to set
.gitignoreLists .env, node_modules and dist, so that they stay out of git
package.json and tsconfig.jsonSet up in Installation
Imports Are Required

Creating a file in src/collections/ does nothing by itself. A collection exists only when config.ts imports it and lists it in collections.

The Files

The Collection File

A collection file imports what it needs, builds the collection and exports it. This is the products collection that the Quickstart ends with:

TypeScript
// src/collections/products.ts
import { collection, defineRoutes, fields } from "@starklabs/forge";
import { z } from "zod";

export const products = collection({
  type: "crud",
  route: "products",
  model: "Product",
  schema: {
    name: fields.requiredString,
    price: fields.requiredNumber,
  },
  validations: {
    create: { name: z.string().min(1).max(120), price: z.number().min(0) },
    update: { name: z.string().min(1).max(120).optional(), price: z.number().min(0).optional() },
  },
  routes: defineRoutes([
    { method: "get", path: "/", handler: "readAll", auth: "public" },
    { method: "get", path: "/:id", handler: "read", auth: "public" },
    { method: "post", path: "/", handler: "create", auth: "authenticated", validation: "create" },
    { method: "patch", path: "/update/:id", handler: "update", auth: "authenticated", validation: "update" },
    { method: "delete", path: "/delete/:id", handler: "remove", auth: "admin" },
  ]),
});

The Config File

config.ts is the one file that knows about the environment. It imports every collection, reads process.env and returns the options:

TypeScript
// src/config.ts
import { fields, type ForgeOptions } from "@starklabs/forge";
import { products } from "./collections/products.js";

export const createOptions = (): ForgeOptions => ({
  ENV: process.env.NODE_ENV,
  port: Number(process.env.PORT ?? 5000),
  mongoDBURI: process.env.MONGODB_URI,
  jwtSecret: process.env.JWT_SECRET,
  frontendURL: process.env.FRONTEND_URL,
  builtinConfig: {
    auth: {
      mode: "builtin",
      signupMode: "credentials",
      loginMode: "credentials",
      mongooseConfig: {
        model: "User",
        schema: {
          name: fields.requiredString,
          email: fields.email,
          password: fields.password,
          role: fields.role,
        },
      },
    },
  },
  collections: [products],
});

The file exports a function, not a ready Forge, so that a test can start from the same options and change what it needs. The Environment Variables page replaces the raw process.env reads with a validated src/env.ts, and Configuration lists every option.

The Server File

TypeScript
// src/server.ts
import { Forge } from "@starklabs/forge";
import { createOptions } from "./config.js";

const forge = new Forge(createOptions());
await forge.start();

The entry point stays small on purpose. The start() method connects to the database, mounts the routes and listens on the port.

A Test File

A test starts the same app on a random port with an in-memory database instead of MongoDB. Spread createOptions() and override the port, because the real port may already be taken by your development server:

TypeScript
// tests/products.test.ts
import { createTestForge, type TestForge } from "@starklabs/forge/testing";
import { afterAll, beforeAll, expect, it } from "vitest";
import { createOptions } from "../src/config.js";

let t: TestForge;

beforeAll(async () => {
  t = await createTestForge({ ...createOptions(), port: 0 });
});

afterAll(async () => {
  await t.close();
});

it("lets anyone list products", async () => {
  const response = await t.client.get("/products");
  expect(response.status).toBe(200);
  expect(response.body.data).toEqual([]);
});

it("refuses to create a product without signing in", async () => {
  const response = await t.client.post("/products", { name: "Desk lamp", price: 34 });
  expect(response.status).toBe(401);
  expect(response.body.code).toBe("ACCESS_TOKEN_MISSING");
});

The helpers in @starklabs/forge/testing replace MongoDB, email delivery and file storage with fakes, and they fill in test defaults such as a JWT secret. An option whose value is undefined, like an unset environment variable, does not replace a default.

Feature Folders Instead

Some teams group files by feature instead of by type. In that layout a resource keeps its collection together with the helpers that only it uses:

  • src
  • products
  • products.collection.ts
  • products.helpers.ts
  • orders
  • orders.collection.ts
  • config.ts
  • server.ts

The rules do not change: config.ts still imports each collection. Choose the layout your team finds easier to navigate and keep it the same across the project. StarkForge requires neither.

Naming Rules

These conventions keep URLs, models and files easy to match:

  • route is plural and lowercase, such as products or order-items. It must start with a letter or a digit and may contain letters, digits, /, _ and -.
  • model is singular and capitalized, such as Product.
  • A collection file exports one collection, named after its route in camelCase, such as products.
  • A file name is kebab-case and matches the route, such as order-items.ts.

Where to Go Next

Environment Variables

Load secrets from .env and validate them once.

Collections

Learn every key of a collection.

:::