Azura Logo
GitHub

RESTful API Example

Learn how to build a complete RESTful API with Azura.

This example demonstrates how to create a RESTful API for a blog with posts and comments. It includes CRUD operations, validation, error handling, and more.

Project Structure

Let's start by setting up the project structure:

plaintext
blog-api/
├── src/
│   ├── controllers/
│   │   ├── post.controller.ts
│   │   └── comment.controller.ts
│   ├── models/
│   │   ├── post.model.ts
│   │   └── comment.model.ts
│   ├── services/
│   │   ├── post.service.ts
│   │   └── comment.service.ts
│   ├── middleware/
│   │   ├── error.middleware.ts
│   │   └── validation.middleware.ts
│   ├── config/
│   │   └── database.ts
│   └── index.ts
├── package.json
└── tsconfig.json

Setup

First, let's set up the project and install the necessary dependencies:

package.json

bash
npm init -y
npm install @atosjs/azura
npm install --save-dev typescript ts-node @types/node

tsconfig.json

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": ["src/**/*"]
}

Models

Let's define the data models for our blog API:

post.model.ts

typescript
// src/models/post.model.ts
export interface Post {
  id: string;
  title: string;
  content: string;
  author: string;
  createdAt: Date;
  updatedAt: Date;
}

export interface CreatePostDto {
  title: string;
  content: string;
  author: string;
}

export interface UpdatePostDto {
  title?: string;
  content?: string;
}

comment.model.ts

typescript
// src/models/comment.model.ts
export interface Comment {
  id: string;
  postId: string;
  content: string;
  author: string;
  createdAt: Date;
}

export interface CreateCommentDto {
  postId: string;
  content: string;
  author: string;
}

Services

Now, let's create the services that will handle the business logic:

post.service.ts

typescript
// src/services/post.service.ts
import { Post, CreatePostDto, UpdatePostDto } from '../models/post.model';

// In-memory database for this example
const posts: Post[] = [];

export class PostService {
  getAllPosts(): Post[] {
    return posts;
  }

  getPostById(id: string): Post | undefined {
    return posts.find(post => post.id === id);
  }

  createPost(createPostDto: CreatePostDto): Post {
    const newPost: Post = {
      id: Date.now().toString(),
      ...createPostDto,
      createdAt: new Date(),
      updatedAt: new Date()
    };
    
    posts.push(newPost);
    return newPost;
  }

  updatePost(id: string, updatePostDto: UpdatePostDto): Post | undefined {
    const postIndex = posts.findIndex(post => post.id === id);
    
    if (postIndex === -1) {
      return undefined;
    }
    
    const updatedPost = {
      ...posts[postIndex],
      ...updatePostDto,
      updatedAt: new Date()
    };
    
    posts[postIndex] = updatedPost;
    return updatedPost;
  }

  deletePost(id: string): boolean {
    const initialLength = posts.length;
    const newPosts = posts.filter(post => post.id !== id);
    
    if (newPosts.length === initialLength) {
      return false;
    }
    
    // Update the posts array
    posts.length = 0;
    posts.push(...newPosts);
    
    return true;
  }
}

comment.service.ts

typescript
// src/services/comment.service.ts
import { Comment, CreateCommentDto } from '../models/comment.model';

// In-memory database for this example
const comments: Comment[] = [];

export class CommentService {
  getCommentsByPostId(postId: string): Comment[] {
    return comments.filter(comment => comment.postId === postId);
  }

  createComment(createCommentDto: CreateCommentDto): Comment {
    const newComment: Comment = {
      id: Date.now().toString(),
      ...createCommentDto,
      createdAt: new Date()
    };
    
    comments.push(newComment);
    return newComment;
  }

  deleteComment(id: string): boolean {
    const initialLength = comments.length;
    const newComments = comments.filter(comment => comment.id !== id);
    
    if (newComments.length === initialLength) {
      return false;
    }
    
    // Update the comments array
    comments.length = 0;
    comments.push(...newComments);
    
    return true;
  }
}

Controllers

Now, let's create the controllers that will handle the HTTP requests:

post.controller.ts

typescript
// src/controllers/post.controller.ts
import { Controller, Get, Post, Put, Delete, Body, Param } from '@atosjs/azura';
import { PostService } from '../services/post.service';
import { CreatePostDto, UpdatePostDto } from '../models/post.model';
import { HttpError } from '@atosjs/azura';

@Controller('/posts')
export class PostController {
  private postService: PostService;

  constructor() {
    this.postService = new PostService();
  }

  @Get('/')
  getAllPosts() {
    return this.postService.getAllPosts();
  }

  @Get('/:id')
  getPostById(@Param('id') id: string) {
    const post = this.postService.getPostById(id);
    
    if (!post) {
      throw new HttpError(404, { message: 'Post not found' });
    }
    
    return post;
  }

  @Post('/')
  createPost(@Body() createPostDto: CreatePostDto) {
    // Validate input
    if (!createPostDto.title || !createPostDto.content || !createPostDto.author) {
      throw new HttpError(400, { message: 'Title, content, and author are required' });
    }
    
    return this.postService.createPost(createPostDto);
  }

  @Put('/:id')
  updatePost(@Param('id') id: string, @Body() updatePostDto: UpdatePostDto) {
    // Validate input
    if (Object.keys(updatePostDto).length === 0) {
      throw new HttpError(400, { message: 'No update data provided' });
    }
    
    const updatedPost = this.postService.updatePost(id, updatePostDto);
    
    if (!updatedPost) {
      throw new HttpError(404, { message: 'Post not found' });
    }
    
    return updatedPost;
  }

  @Delete('/:id')
  deletePost(@Param('id') id: string) {
    const deleted = this.postService.deletePost(id);
    
    if (!deleted) {
      throw new HttpError(404, { message: 'Post not found' });
    }
    
    return { message: 'Post deleted successfully' };
  }
}

comment.controller.ts

typescript
// src/controllers/comment.controller.ts
import { Controller, Get, Post, Delete, Body, Param } from '@atosjs/azura';
import { CommentService } from '../services/comment.service';
import { PostService } from '../services/post.service';
import { CreateCommentDto } from '../models/comment.model';
import { HttpError } from '@atosjs/azura';

@Controller('/comments')
export class CommentController {
  private commentService: CommentService;
  private postService: PostService;

  constructor() {
    this.commentService = new CommentService();
    this.postService = new PostService();
  }

  @Get('/post/:postId')
  getCommentsByPostId(@Param('postId') postId: string) {
    // Check if post exists
    const post = this.postService.getPostById(postId);
    
    if (!post) {
      throw new HttpError(404, { message: 'Post not found' });
    }
    
    return this.commentService.getCommentsByPostId(postId);
  }

  @Post('/')
  createComment(@Body() createCommentDto: CreateCommentDto) {
    // Validate input
    if (!createCommentDto.postId || !createCommentDto.content || !createCommentDto.author) {
      throw new HttpError(400, { message: 'PostId, content, and author are required' });
    }
    
    // Check if post exists
    const post = this.postService.getPostById(createCommentDto.postId);
    
    if (!post) {
      throw new HttpError(404, { message: 'Post not found' });
    }
    
    return this.commentService.createComment(createCommentDto);
  }

  @Delete('/:id')
  deleteComment(@Param('id') id: string) {
    const deleted = this.commentService.deleteComment(id);
    
    if (!deleted) {
      throw new HttpError(404, { message: 'Comment not found' });
    }
    
    return { message: 'Comment deleted successfully' };
  }
}

Middleware

Let's create some middleware for error handling and request logging:

error.middleware.ts

typescript
// src/middleware/error.middleware.ts
import { Request, Response, NextFunction } from '@atosjs/azura';
import { HttpError } from '@atosjs/azura';

export function errorMiddleware(err: Error, req: Request, res: Response, next: NextFunction) {
  console.error('Error:', err);
  
  if (err instanceof HttpError) {
    return res.status(err.status).json(err.payload);
  }
  
  // Default error response for unhandled errors
  return res.status(500).json({
    message: 'Internal Server Error',
    error: process.env.NODE_ENV === 'production' ? undefined : err.message
  });
}

logging.middleware.ts

typescript
// src/middleware/logging.middleware.ts
import { Request, Response, NextFunction } from '@atosjs/azura';

export function loggingMiddleware(req: Request, res: Response, next: NextFunction) {
  const start = Date.now();
  
  // Add a listener for when the response finishes
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
  });
  
  next();
}

Main Application File

Finally, let's create the main application file that ties everything together:

index.ts

typescript
// src/index.ts
import { AzuraServer } from '@atosjs/azura';
import { PostController } from './controllers/post.controller';
import { CommentController } from './controllers/comment.controller';
import { errorMiddleware } from './middleware/error.middleware';
import { loggingMiddleware } from './middleware/logging.middleware';

async function bootstrap() {
  const app = new AzuraServer({
    port: process.env.PORT ? parseInt(process.env.PORT) : 3000
  });
  
  // Register middleware
  app.use(loggingMiddleware);
  
  // Register controllers
  app.load([
    PostController,
    CommentController
  ]);
  
  // Register error middleware (should be last)
  app.use(errorMiddleware);
  
  // Start the server
  app.listen();
  console.log(`Server is running on http://localhost:${app.config.port}`);
}

bootstrap().catch(console.error);

Testing the API

Now that we have our API set up, let's test it with some HTTP requests:

Create a Post

bash
# Create a post
curl -X POST http://localhost:3000/posts \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Hello Azura",
    "content": "This is my first post with Azura.JS Framework",
    "author": "John Doe"
  }'

Get All Posts

bash
# Get all posts
curl http://localhost:3000/posts

Get a Specific Post

bash
# Get a specific post
curl http://localhost:3000/posts/1234567890

Update a Post

bash
# Update a post
curl -X PUT http://localhost:3000/posts/1234567890 \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Updated Title",
    "content": "Updated content for my post"
  }'

Delete a Post

bash
# Delete a post
curl -X DELETE http://localhost:3000/posts/1234567890

Create a Comment

bash
# Create a comment
curl -X POST http://localhost:3000/comments \
  -H "Content-Type: application/json" \
  -d '{
    "postId": "1234567890",
    "content": "Great post!",
    "author": "Jane Smith"
  }'

Get Comments for a Post

bash
# Get comments for a post
curl http://localhost:3000/comments/post/1234567890

Conclusion

In this example, we've built a complete RESTful API for a blog with posts and comments. We've covered:

  • Setting up the project structure
  • Defining data models and DTOs
  • Creating services for business logic
  • Implementing controllers with decorators
  • Adding middleware for error handling and logging
  • Testing the API with HTTP requests