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.
The Recommended Layout
- acme-api
- src
- collections
- products.ts
- config.ts
- server.ts
- tests
- products.test.ts
- .env
- .env.example
- .gitignore
- package.json
- tsconfig.json
| Path | What 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.ts | createOptions(), which reads process.env and returns the options for new Forge, including the list of collections |
src/server.ts | The entry point. It creates the Forge and starts it |
tests/ | Tests. They build the same app from createOptions() and a fake database |
.env | The real values for your machine. Never commit it |
.env.example | The same names with fake values, committed so that other developers know what to set |
.gitignore | Lists .env, node_modules and dist, so that they stay out of git |
package.json and tsconfig.json | Set up in Installation |
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:
// 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:
// 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
// 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:
// 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:
routeis plural and lowercase, such asproductsororder-items. It must start with a letter or a digit and may contain letters, digits,/,_and-.modelis singular and capitalized, such asProduct.- 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.
:::