TypeScript backend setup typically requires managing multiple configuration files: package managers, dependencies, tsconfig.json, bundlers, hot-reloading utilities, and routing boilerplates. Additionally, database integration introduces the need for Object-Relational Mappers (ORMs), schema migrations, and connection pool management.
This overhead slows down the development of microservices, webhooks, and mobile backends.
Typebase bypasses this setup phase. It uses file-based routing and integrated storage to let you build APIs from a single folder with zero initial configuration.
The Single-Folder Architecture
Typebase eliminates controllers, services, and repositories. The folder structure itself defines the API endpoints and the data models.
Consider this directory structure:
my-service/
├── api/
│ ├── users/
│ │ ├── get.ts
│ │ └── post.ts
│ ├── items/
│ │ ├── [id]/
│ │ │ └── get.ts
│ │ └── get.ts
│ └── status.ts
└── data/
└── users.jsonThe api/ directory holds the route handlers. The file path maps directly to the URL path, and the filename maps to the HTTP method. For example:
api/users/post.tshandlesPOST /usersapi/items/[id]/get.tshandlesGET /items/:idapi/status.tshandlesGET /status(defaulting to GET when no method is specified in the filename)
The data/ directory holds the application state. You do not need to run a separate database server. Typebase manages local JSON files or SQLite databases mapped directly to TypeScript interfaces.
To start the development server, run:
npx typebase devThe engine reads the directory structure, starts the HTTP server, and watches for file changes. Route edits and new files are registered instantly without restarting the process.
Route Resolution Algorithm
Under the hood, Typebase parses the api/ directory at startup and builds a route tree. When an HTTP request arrives, the engine matches the request path against the tree using the following priority rules:
- Exact Matches: Static paths like
api/items/get.tstake precedence. - Dynamic Matches: Parameterized paths like
api/items/[id]/get.tsmatch if no static route matches. - Catch-All Matches: Paths like
api/items/[...catchall].tsmatch any remaining sub-paths.
Here is a simplified representation of the internal route matching logic:
function matchRoute(requestPath: string, method: string, routes: Route[]) {
const segments = requestPath.split('/').filter(Boolean);
for (const route of routes) {
if (route.method !== method) continue;
const routeSegments = route.path.split('/').filter(Boolean);
if (routeSegments.length !== segments.length && !route.isCatchAll) continue;
const params: Record<string, string> = {};
let match = true;
for (let i = 0; i < routeSegments.length; i++) {
const routeSeg = routeSegments[i];
const reqSeg = segments[i];
if (routeSeg.startsWith('[') && routeSeg.endsWith(']')) {
const paramName = routeSeg.slice(1, -1);
params[paramName] = reqSeg;
} else if (routeSeg !== reqSeg) {
match = false;
break;
}
}
if (match) {
return { handler: route.handler, params };
}
}
return null;
}Writing Route Handlers
Handlers export a default asynchronous function. The engine passes request and response objects to this function.
Example api/users/get.ts:
import { Request, Response } from 'typebase';
interface User {
id: string;
name: string;
role: string;
}
export default async function handler(req: Request, res: Response) {
const users: User[] = [
{ id: '1', name: 'Alice', role: 'admin' },
{ id: '2', name: 'Bob', role: 'user' }
];
return res.json(users);
}You do not need to import Express, configure middleware, or maintain a central route registry. The presence of the file registers the endpoint.
Dynamic paths use brackets. Example api/items/[id]/get.ts:
import { Request, Response } from 'typebase';
export default async function handler(req: Request, res: Response) {
const itemId = req.params.id;
if (!itemId) {
return res.status(400).json({ error: 'Missing item ID' });
}
return res.json({
id: itemId,
name: `Item ${itemId}`,
status: 'active'
});
}The engine parses the incoming URL, extracts the parameters, and populates the req.params object before invoking the handler.
Middleware and Request Lifecycle
Typebase supports route-specific middleware by exporting a middleware array from the route file. The engine executes these functions sequentially before running the main handler.
Example of a protected route in api/admin/get.ts:
import { Request, Response, NextFunction } from 'typebase';
export const middleware = [
async function authenticate(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized' });
}
const token = authHeader.split(' ')[1];
if (token !== 'secret-token') {
return res.status(403).json({ error: 'Forbidden' });
}
next();
}
];
export default async function handler(req: Request, res: Response) {
return res.json({ status: 'secure-data' });
}Input Validation at the Edge
Typebase includes built-in schema validation. You define the validation schema directly in the route file. The engine validates the request body, query parameters, and headers before the handler runs.
Example api/users/post.ts:
import { Request, Response, Schema } from 'typebase';
export const schema: Schema = {
body: {
name: { type: 'string', required: true, minLength: 2 },
email: { type: 'string', required: true, format: 'email' },
age: { type: 'number', required: false },
profile: {
type: 'object',
required: true,
properties: {
bio: { type: 'string', maxLength: 160 }
}
}
}
};
interface CreateUserBody {
name: string;
email: string;
age?: number;
profile: {
bio: string;
};
}
export default async function handler(req: Request<CreateUserBody>, res: Response) {
const { name, email, age, profile } = req.body;
const newUser = {
id: crypto.randomUUID(),
name,
email,
age,
profile
};
return res.status(201).json(newUser);
}If a request fails validation, the engine returns a 400 Bad Request status code with a structured error payload:
{
"error": "Validation Failed",
"details": [
{ "field": "body.email", "message": "Value must be a valid email address" },
{ "field": "body.profile", "message": "Required field is missing" }
]
}The handler function only runs if the incoming data matches the defined schema.
File-Based Data Storage
Typebase includes an integrated data store. By default, it saves data to local JSON files. For larger datasets or transactional workloads, you can configure it to use SQLite.
Example of using the database API:
import { Request, Response, db } from 'typebase';
interface Task {
id: string;
title: string;
completed: boolean;
createdAt: string;
}
export default async function handler(req: Request, res: Response) {
const tasksCollection = await db.collection<Task>('tasks');
if (req.method === 'GET') {
const allTasks = await tasksCollection.find({ completed: false });
return res.json(allTasks);
}
if (req.method === 'POST') {
const body = req.body as { title: string };
if (!body.title) {
return res.status(400).json({ error: 'Title is required' });
}
const newTask = await tasksCollection.insert({
id: crypto.randomUUID(),
title: body.title,
completed: false,
createdAt: new Date().toISOString()
});
return res.status(201).json(newTask);
}
}Calling db.collection<Task>('tasks') initializes the collection. If the file data/tasks.json does not exist, Typebase creates it.
Concurrency and Storage Drivers
To prevent data corruption during concurrent writes, Typebase routes all write operations through an in-memory queue. This queue serializes disk writes, ensuring that concurrent requests do not overwrite each other's changes.
For production workloads, you can switch the storage driver to SQLite by creating a typebase.config.json file in the root directory:
{
"database": {
"driver": "sqlite",
"storage": "data/production.sqlite",
"wal": true
}
}Enabling Write-Ahead Logging (WAL) mode allows concurrent reads to proceed while a write operation is in progress, improving throughput.
How Typebase Runs TypeScript Without a Build Step
Node.js cannot execute TypeScript files directly without an external compilation step. Typebase solves this by wrapping esbuild.
During development, when a request hits a route:
- The engine checks if the handler file has changed since the last compilation.
- If changed,
esbuildcompiles the file in memory. - The compiled JavaScript is loaded using a dynamic import.
- The result is cached in memory for subsequent requests.
This process keeps startup times low and ensures that changes to route files are reflected immediately on the next request.
For production deployments, compile the project beforehand:
npx typebase buildThis command compiles all routes, bundles external dependencies, and outputs the optimized assets to the dist/ directory. You can then run the production server using standard Node.js:
node dist/server.jsThis build step keeps the production container small, applying principles similar to frontend bundle optimization, and removes the need to ship esbuild or TypeScript compilers to the production environment.
Comparing Typebase with Other Frameworks
| Feature | Typebase | Express + TS | Next.js API Routes |
|---|---|---|---|
| Routing | File-based | Code-based | File-based |
| Setup Time | Zero | High | Medium |
| Built-in DB | Yes | No | No |
| Execution | On-demand TS | Build required | Next.js dev server |
| Bundle Size | Minimal | Variable | Large (Next.js core) |
Next.js API routes carry a large dependency footprint because they are optimized for React applications, even when utilizing features like Next.js 15 partial prerendering. Express requires manual routing setup, server configuration, and TypeScript build pipelines. Typebase provides file-based routing with a minimal runtime footprint and built-in storage.
Limitations and Trade-offs
Typebase is designed for small to medium services. It has specific limitations:
- Complex Middleware: Global middleware chains and complex request lifecycles are harder to manage without a central application registry.
- Query Performance: The built-in JSON database does not support complex queries, deep nesting, or ACID transactions across multiple collections. For these use cases, connect to an external database like PostgreSQL or MySQL.
- Cold Starts: The first request to a route during development incurs a 10 to 50 ms latency penalty due to on-the-fly compilation. Subsequent requests run at native speed.
Deploying a Typebase Service
To deploy a Typebase service, copy the project directory to your hosting provider.
Here is a production-ready Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npx typebase build
EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "dist/server.js"]If you use the built-in file storage, mount a persistent volume to the /app/data directory to prevent data loss when the container restarts. For serverless deployments, configure the application to use an external database.



