REST API Example
A complete REST API example using Balda.js with validation, serialization, and error handling.
Project Structure
src/
├── controllers/
│ ├── users.controller.ts
│ ├── posts.controller.ts
│ └── auth.controller.ts
├── middleware/
│ ├── auth.middleware.ts
│ └── validation.middleware.ts
├── schemas/
│ ├── user.schema.ts
│ └── post.schema.ts
├── services/
│ ├── user.service.ts
│ └── post.service.ts
└── server.ts
Server Setup
// src/server.ts
import { Server } from 'balda-js';
const server = new Server({
port: 3000,
host: 'localhost',
controllerPatterns: ['./src/controllers/**/*.ts'],
plugins: {
cors: {
origin: ['http://localhost:3000', 'http://localhost:3001'],
credentials: true
},
json: {
sizeLimit: '10mb',
strict: true
},
cookie: {
secret: process.env.COOKIE_SECRET || 'dev-secret',
secure: false,
httpOnly: true
},
helmet: {
contentSecurityPolicy: false
},
log: {
logRequest: true,
logResponse: true
}
},
swagger: {
type: 'standard',
models: {
User: {
type: 'object',
properties: {
id: { type: 'number' },
email: { type: 'string' },
name: { type: 'string' },
createdAt: { type: 'string', format: 'date-time' }
}
},
Post: {
type: 'object',
properties: {
id: { type: 'number' },
title: { type: 'string' },
content: { type: 'string' },
authorId: { type: 'number' },
createdAt: { type: 'string', format: 'date-time' }
}
}
}
}
});
// Global error handler
server.setErrorHandler((req, res, next, error) => {
console.error('Error:', error);
if (error.name === 'ValidationError') {
return res.badRequest({
error: 'Validation failed',
details: error.message
});
}
if (error.name === 'UnauthorizedError') {
return res.unauthorized({ error: 'Authentication required' });
}
res.internalServerError({ error: 'Internal server error' });
});
// Custom not found handler for API responses
server.setNotFoundHandler((req, res) => {
console.warn(`404 Not Found: ${req.method} ${req.url}`);
res.status(404).json({
error: 'Not Found',
message: 'The requested endpoint does not exist',
path: req.url,
method: req.method,
timestamp: new Date().toISOString(),
availableEndpoints: [
'GET /users',
'POST /users',
'GET /users/:id',
'PUT /users/:id',
'DELETE /users/:id',
'POST /users/login',
'GET /posts',
'POST /posts',
'GET /posts/:id',
'PUT /posts/:id',
'DELETE /posts/:id',
'GET /posts/author/:authorId'
]
});
});
server.listen(({ port, host }) => {
console.log(`🚀 Server running on http://${host}:${port}`);
console.log(`📚 API Documentation: http://${host}:${port}/docs`);
});
Data Schemas
// src/schemas/user.schema.ts
import z from 'zod';
export const CreateUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(1).max(100)
});
export const UpdateUserSchema = CreateUserSchema.partial();
export const UserSchema = z.object({
id: z.number(),
email: z.string().email(),
name: z.string(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime()
});
export const LoginSchema = z.object({
email: z.string().email(),
password: z.string()
});
export type CreateUser = z.infer<typeof CreateUserSchema>;
export type UpdateUser = z.infer<typeof UpdateUserSchema>;
export type User = z.infer<typeof UserSchema>;
export type Login = z.infer<typeof LoginSchema>;
// src/schemas/post.schema.ts
import z from 'zod';
export const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
published: z.boolean().optional()
});
export const UpdatePostSchema = CreatePostSchema.partial();
export const PostSchema = z.object({
id: z.number(),
title: z.string(),
content: z.string(),
published: z.boolean(),
authorId: z.number(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime()
});
export type CreatePost = z.infer<typeof CreatePostSchema>;
export type UpdatePost = z.infer<typeof UpdatePostSchema>;
export type Post = z.infer<typeof PostSchema>;
Services
// src/services/user.service.ts
import { User, CreateUser, UpdateUser } from '../schemas/user.schema';
export class UserService {
private users: User[] = [];
private nextId = 1;
async findAll(): Promise<User[]> {
return this.users;
}
async findById(id: number): Promise<User | null> {
return this.users.find(user => user.id === id) || null;
}
async findByEmail(email: string): Promise<User | null> {
return this.users.find(user => user.email === email) || null;
}
async create(data: CreateUser): Promise<User> {
const existingUser = await this.findByEmail(data.email);
if (existingUser) {
throw new Error('User with this email already exists');
}
const user: User = {
id: this.nextId++,
email: data.email,
name: data.name,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
this.users.push(user);
return user;
}
async update(id: number, data: UpdateUser): Promise<User | null> {
const userIndex = this.users.findIndex(user => user.id === id);
if (userIndex === -1) {
return null;
}
const updatedUser = {
...this.users[userIndex],
...data,
updatedAt: new Date().toISOString()
};
this.users[userIndex] = updatedUser;
return updatedUser;
}
async delete(id: number): Promise<boolean> {
const userIndex = this.users.findIndex(user => user.id === id);
if (userIndex === -1) {
return false;
}
this.users.splice(userIndex, 1);
return true;
}
}
export const userService = new UserService();
// src/services/post.service.ts
import { Post, CreatePost, UpdatePost } from '../schemas/post.schema';
export class PostService {
private posts: Post[] = [];
private nextId = 1;
async findAll(published?: boolean): Promise<Post[]> {
if (published !== undefined) {
return this.posts.filter(post => post.published === published);
}
return this.posts;
}
async findById(id: number): Promise<Post | null> {
return this.posts.find(post => post.id === id) || null;
}
async findByAuthor(authorId: number): Promise<Post[]> {
return this.posts.filter(post => post.authorId === authorId);
}
async create(data: CreatePost, authorId: number): Promise<Post> {
const post: Post = {
id: this.nextId++,
title: data.title,
content: data.content,
published: data.published || false,
authorId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
this.posts.push(post);
return post;
}
async update(id: number, data: UpdatePost): Promise<Post | null> {
const postIndex = this.posts.findIndex(post => post.id === id);
if (postIndex === -1) {
return null;
}
const updatedPost = {
...this.posts[postIndex],
...data,
updatedAt: new Date().toISOString()
};
this.posts[postIndex] = updatedPost;
return updatedPost;
}
async delete(id: number): Promise<boolean> {
const postIndex = this.posts.findIndex(post => post.id === id);
if (postIndex === -1) {
return false;
}
this.posts.splice(postIndex, 1);
return true;
}
}
export const postService = new PostService();
Middleware
// src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'balda-js';
import { userService } from '../services/user.service';
export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.unauthorized({ error: 'Authentication token required' });
}
try {
// In a real app, you'd verify the JWT token here
// For this example, we'll use a simple user ID lookup
const userId = parseInt(token);
const user = await userService.findById(userId);
if (!user) {
return res.unauthorized({ error: 'Invalid authentication token' });
}
req.user = user;
next();
} catch (error) {
return res.unauthorized({ error: 'Invalid authentication token' });
}
};
export const optionalAuthMiddleware = async (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (token) {
try {
const userId = parseInt(token);
const user = await userService.findById(userId);
if (user) {
req.user = user;
}
} catch (error) {
// Ignore auth errors for optional auth
}
}
next();
};
Controllers
// src/controllers/users.controller.ts
import {
controller,
get,
post,
put,
del,
validate,
serialize,
middleware
} from 'balda-js';
import { Request, Response } from 'balda-js';
import {
CreateUserSchema,
UpdateUserSchema,
UserSchema,
LoginSchema
} from '../schemas/user.schema';
import { userService } from '../services/user.service';
import { authMiddleware } from '../middleware/auth.middleware';
import z from 'zod';
@controller('/users')
export class UsersController {
@get('/')
@serialize(z.array(UserSchema))
async getAllUsers(req: Request, res: Response) {
const users = await userService.findAll();
res.json(users);
}
@get('/:id')
@serialize(UserSchema)
async getUserById(req: Request, res: Response) {
const id = parseInt(req.params.id);
const user = await userService.findById(id);
if (!user) {
return res.notFound({ error: 'User not found' });
}
res.json(user);
}
@post('/')
@validate.body(CreateUserSchema)
@serialize(UserSchema)
async createUser(req: Request, res: Response, body: CreateUser) {
try {
const user = await userService.create(body);
res.created(user);
} catch (error) {
return res.conflict({ error: error.message });
}
}
@put('/:id')
@middleware(authMiddleware)
@validate.body(UpdateUserSchema)
@serialize(UserSchema)
async updateUser(req: Request, res: Response, body: UpdateUser) {
const id = parseInt(req.params.id);
// Ensure users can only update their own profile
if (req.user.id !== id) {
return res.forbidden({ error: 'Cannot update other users' });
}
const user = await userService.update(id, body);
if (!user) {
return res.notFound({ error: 'User not found' });
}
res.json(user);
}
@del('/:id')
@middleware(authMiddleware)
async deleteUser(req: Request, res: Response) {
const id = parseInt(req.params.id);
// Ensure users can only delete their own account
if (req.user.id !== id) {
return res.forbidden({ error: 'Cannot delete other users' });
}
const deleted = await userService.delete(id);
if (!deleted) {
return res.notFound({ error: 'User not found' });
}
res.noContent();
}
@post('/login')
@validate.body(LoginSchema)
async login(req: Request, res: Response, body: Login) {
const user = await userService.findByEmail(body.email);
if (!user) {
return res.unauthorized({ error: 'Invalid credentials' });
}
// In a real app, you'd verify the password hash here
// For this example, we'll just return the user ID as a token
res.json({
token: user.id.toString(),
user: {
id: user.id,
email: user.email,
name: user.name
}
});
}
}
// src/controllers/posts.controller.ts
import {
controller,
get,
post,
put,
del,
validate,
serialize,
middleware,
Request,
Response
} from 'balda-js';
import z from 'zod';
import {
CreatePostSchema,
UpdatePostSchema,
PostSchema
} from '../schemas/post.schema';
import { postService } from '../services/post.service';
import { authMiddleware, optionalAuthMiddleware } from '../middleware/auth.middleware';
@controller('/posts')
export class PostsController {
@get('/')
@middleware(optionalAuthMiddleware)
@serialize(z.array(PostSchema))
async getAllPosts(req: Request, res: Response) {
const published = req.query.published === 'true';
const posts = await postService.findAll(published);
res.json(posts);
}
@get('/:id')
@serialize(PostSchema)
async getPostById(req: Request, res: Response) {
const id = parseInt(req.params.id);
const post = await postService.findById(id);
if (!post) {
return res.notFound({ error: 'Post not found' });
}
// Only show unpublished posts to their authors
if (!post.published && (!req.user || req.user.id !== post.authorId)) {
return res.notFound({ error: 'Post not found' });
}
res.json(post);
}
@post('/')
@middleware(authMiddleware)
@validate.body(CreatePostSchema)
@serialize(PostSchema)
async createPost(req: Request, res: Response, body: z.infer<typeof CreatePostSchema>) {
const post = await postService.create(body, req.user.id);
res.created(post);
}
@put('/:id')
@middleware(authMiddleware)
@validate.body(UpdatePostSchema)
@serialize(PostSchema)
async updatePost(req: Request, res: Response, body: UpdatePost) {
const id = parseInt(req.params.id);
const existingPost = await postService.findById(id);
if (!existingPost) {
return res.notFound({ error: 'Post not found' });
}
// Ensure users can only update their own posts
if (req.user.id !== existingPost.authorId) {
return res.forbidden({ error: 'Cannot update other users\' posts' });
}
const post = await postService.update(id, body);
res.json(post);
}
@del('/:id')
@middleware(authMiddleware)
async deletePost(req: Request, res: Response) {
const id = parseInt(req.params.id);
const existingPost = await postService.findById(id);
if (!existingPost) {
return res.notFound({ error: 'Post not found' });
}
// Ensure users can only delete their own posts
if (req.user.id !== existingPost.authorId) {
return res.forbidden({ error: 'Cannot delete other users\' posts' });
}
const deleted = await postService.delete(id);
res.noContent();
}
@get('/author/:authorId')
@serialize(z.array(PostSchema))
async getPostsByAuthor(req: Request, res: Response) {
const authorId = parseInt(req.params.authorId);
const posts = await postService.findByAuthor(authorId);
// Filter out unpublished posts unless the user is the author
const filteredPosts = posts.filter(post =>
post.published || (req.user && req.user.id === authorId)
);
res.json(filteredPosts);
}
}