On day 27, I completed Vue 3 Composition API course to build a simple blog to show blog posts. The site calls https://jsonplaceholder.typicode.com/posts to retrieve all the posts and display the users.
The blog posts are built in 3 parts:
- Part 1: Create a usePost composable to retrieve all posts and a post
- Part 2: Create a useUser composable to retrieve a user. Use
watchto monitor post and retrieve a user - Part 3: Build a reusable composable to retrieve Post and User resources
- Part 4: Add a loading indicator
Install Tailwindcss
Refer to https://tailwindcss.com/docs/installation/framework-guides to install TailwindCSS for Vue 3, Svelte 5 and Angular.
Copy Vue boilerplates
Copy the boilerplates from https://github.com/vueschool/vue-3-composition-api/tree/boilerplate/ to your Vue repo. The author wrote the codes in JavaScript and without the <script setup lang="ts"> syntactic sugar, so I rewrote it with TypeScript.
Create Common Types
Create a Post type to retrieve id, title, body and userId.
export type Post = { userId: number; id: number; title: string; body: string; } Create a User type to retrieve id, name.
export type User = { id: number; name: string; } Define the Routes
Vue 3 application
import { createRouter, createWebHistory } from 'vue-router' import Home from '../views/Home.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'Home', component: Home, }, { path: '/post/:id', name: 'Post', component: () => import('../views/Post.vue'), }, ], }) export default router I have not learned Vue Router yet, so I use the generated routes of the Vue 3 app.
SvelteKit application
SvelteKit uses file based routing.
routes/ |-----posts/[id] |-----+layout.svelte |-----+page.svelte |-----+page.server.ts |-----+layout.svelte |-----+page.server.ts |-----+page.svelte The home page is designed in +page.svelte and +page.server.ts defines a load function to retrieve all posts on the server side.
Under posts/[id] folder, +page.svelt is the details page and +page.ts retrieves a specific post by id.
Angular 20 application
import { Routes } from '@angular/router'; import { postResolver } from './post/resolvers/post.resolver'; export const routes: Routes = [ { path: 'home', loadComponent: () => import('./home/home.component').then(m => m.HomeComponent), }, { path: 'post/:id', loadComponent: () => import('./post/post.component'), }, ... other routes ... ]; Create home and post/:id routes that load the HomeComponent and PostComponent lazily.
Load Posts and Post data
Vue 3 application
Create ausePost composable under src/composables folder.
Implement a fetchAll function and declare a posts ref to retrieve all posts.
import type { Post } from '@/types/post' import { ref } from 'vue' export function usePost() { const posts = ref<Post[]>([]) const baseUrl = 'https://jsonplaceholder.typicode.com/posts' function fetchAll() { return fetch(baseUrl) .then((response) => response.json() as Promise<Post[]>) .then((data) => (posts.value = data)) .catch((err) => alert(err)) } return { posts, fetchAll, } } The fetchAll makes a request to the baseUrl to retrieve all posts and assign the posts toposts.value.
import type { Post } from '@/types/post' import { ref } from 'vue' export function usePost() { const post = ref<Post | null>(null) function fetchOne(id: number) { return fetch(`${baseUrl}/${id}`) .then((response) => response.json() as Promise<Post>) .then((data) => (post.value = data)) } return { posts, post, fetchAll, fethcOne, } } Next, declare a post ref and implement a fetchOne function. The fetchOne function accepts a post ID to retrieve a blog post and assigns the result to post.value.
Then, the composable returns all posts, post, fetchAll and fetchOne so the components can accesss.
SvelteKit application
export const BASE_URL = 'https://jsonplaceholder.typicode.com'; import type { PageServerLoad } from './$types'; import { BASE_URL } from '$lib/constants/posts.const'; import type { Post } from '$lib/types/post'; import type { RequestHandler } from '@sveltejs/kit'; // retreive all posts export const load: PageServerLoad = async ({ fetch }: RequestHandler) => { const posts = await fetch(`${BASE_URL}/posts`) .then((response) => response.json() as Promise<Post[]>) .catch((error) => { console.error('Error fetching posts:', error); return [] as Post[]; }); return { posts }; }; The load function uses the native fetch function to retrieve all blog posts.
import { BASE_URL } from '$lib/constants/posts.const'; import type { Post } from '$lib/types/post'; import type { PageServerLoad } from './$types'; // retreive a post by an ID export const load: PageServerLoad = async ({ params, fetch }): Promise<{ post: Post | undefined }> => { console.log('params', params); const post = await fetch(`${BASE_URL}/posts/${params.id}`) .then((response) => response.json() as Promise<Post>) .catch((error) => { console.error('Error fetching posts:', error); return undefined; }); return { post }; }; This load function runs on the server and makes a HTTP request to retrieve a post by an ID.
Angular 20 application
Provide the provideHttpClient provider in the ApplicationConfig.
import { provideRouter, withComponentInputBinding } from '@angular/router'; import { routes } from './app.routes'; import { provideHttpClient } from '@angular/common/http'; export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes, withComponentInputBinding()), provideHttpClient(), ] }; It is because httpResource uses HttpClient under the hood and I also need it to make a GET request to retrieve a post.
The provideRouter is a provider that configures the routes. withComponentInputBinding is a feature that converts routed data, path parameters, and query parameters to input signala. It simplifies routing data to routed components immensely.
Create a PostsService.
import { httpResource } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Post } from '../types/post.type'; const BASE_URL = 'https://jsonplaceholder.typicode.com/posts'; @Injectable({ providedIn: 'root' }) export class PostsService { posts = httpResource<Post[]>(() => BASE_URL, { defaultValue: [] as Post[] }); } I use the experimental httpResource to create a posts resource to retrieve all posts.
import { httpResource } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Post } from '../types/post.type'; const BASE_URL = 'https://jsonplaceholder.typicode.com/posts'; @Injectable({ providedIn: 'root' }) export class PostsService { readonly httpService = inject(HttpClient); getPost(id: number): Observable<Post> { return this.httpService.get<Post>(`${BASE_URL}/${id}`); } } I implemented a getPost method to retrieve a post. Thie method will be used in a route resolver during route navigation.
import { inject } from '@angular/core'; import { ActivatedRouteSnapshot } from '@angular/router'; import { of } from 'rxjs'; import { PostsService } from '../services/posts.service'; export const postResolver = (route: ActivatedRouteSnapshot) => { const postId = route.paramMap.get('id'); if (!postId) { return of(undefined); } return inject(PostsService).getPost(+postId); } Create a postResolver that retrieves a post by the ID path parameter.
Update the post/:id route to calls the the postResolver.
{ path: 'post/:id', loadComponent: () => import('./post/post.component'), resolve: { post: postResolver } } The PostComponent has post input signal that is a Post.
Replace the Post mock data with the fetch calls
Vue 3 application
In the Home component, import the usePost composable and destructure fetchAll and posts.
<script setup lang="ts"> import PostCard from '../components/PostCard.vue' import { usePost } from '@/composables/usePost' const { posts, fetchAll } = usePost() fetchAll() </script> Invoke the fetchAll function to retrieve all posts asynchronously. Then, delete the mock data of the posts array to display the real data.
<template> <div class="flex flex-wrap flex-grow"> <PostCard v-for="post in items" :key="post.id" :post="post" /> </div> </template> In the Post component, import usePost and invoke fetchOne to retrieve a post by an ID.
import { useRoute } from 'vue-router' import { usePost } from '@/composables/usePost' const { post, fetchOne } = usePost() const { params } = useRoute(); fetchOne(+params.id) SvelteKit application
In routes/+page.svelte, the load function is executed and the page data can be obtained by $props() macro.
Destructure posts from data to get all the posts.
<script lang="ts"> import type { PageProps } from './$types'; import PostCard from '$lib/components/post-card.svelte'; const { data }: PageProps = $props(); const { posts } = data; </script> <div class="flex flex-grow flex-wrap"> {#each posts as post (post.id)} <PostCard {post} /> {/each} </div> The template iterates the posts and passes each post to the PostCard component.
<script lang="ts"> import type { Post } from '$lib/types/post'; import { resolve } from '$app/paths'; type Props = { post: Post; }; const { post }: Props = $props(); const postUrl = resolve('/posts/[id]', { id: `${post.id}` }); </script> The PostCard component uses the resolve function to determine the URL for navigation. /posts/[id] is the route path and id is the path parameter.
<div> <img src="https://placehold.co/150" alt="placeholder" style="background: #cccccc" width="150" height="150" /> <a href={postUrl}> {post.title} </a> </div> <script lang="ts"> import type { PageProps } from './$types'; const { data }: PageProps = $props(); const { post } = data; </script> In posts/[id]/+page.svelte, the load function retrieves a post by an ID.
<div class="mb-10"> <h1 class="text-3xl">{ post.title }</h1> <div class="text-gray-500 mb-10">by Connie</div> <div class="mb-10">{ post.body }</div> </div> The template displays post title and body dynamically, and the user name is hardcoded currently.
Angular 20 application
import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core'; import { Post } from './types/post.type'; import { User } from './types/user.type'; @Component({ selector: 'app-post', styles: ` @reference "../../styles.css"; :host { @apply flex m-2 gap-2 items-center w-1/4 shadow-md flex-grow rounded overflow-hidden } `, template: `...inline template...`, changeDetection: ChangeDetectionStrategy.OnPush, }) export default class PostComponent { post = input<Post>(); user = signal<User>({ id: 1, name: 'Connie', }); } The post is resolved during route navigation, so it is available to the PostComponent as an input signal. The user signal is currently hardcoded value but a request will be made to retrieve the post user.
@let myUser = user(); @let myPost = post(); @if (myPost && myUser) { <div class="mb-10"> <h1 class="text-3xl">{{ myPost.title }}</h1> <div class="text-gray-500 mb-10">by {{ myUser.name }}</div> <div class="mb-10">{{ myPost.body }}</div> </div> } The @let syntax allows temporary assignment of the signal getter functions. When myUser and myPost are defined, the post title, post body, and user name are displayed.
We have successfully displayed the home page, details page, and set up the routing of this simple blog.
Github Repositories
- Vue 3: https://github.com/railsstudent/vue-example-blog
- Svelte 5: https://github.com/railsstudent/svelte-example-blog
- Angular 20: https://github.com/railsstudent/angular-example-blog
Resources
- JSON Placeholder API: https://jsonplaceholder.typicode.com/
- Basic SvelteKit Tutorial: https://svelte.dev/tutorial/kit/page-data
Top comments (0)