Sveltekit Todoapps API
Kali ini kita akan membuat todoapps api menggunakan svelkit, drizzle-orm dan playwright untuk test rest api.
Requirement
- svelkit (js framework)
- playwright (test package)
- drizzle-orm (orm)
- drizzle-kit (orm cli helper)
- mysql2 (mysql driver)
- joi (validation package)
- bcrypt (hash package)
- winston (logger package)
- uuid (for generate token)
Setup
create project
pertama kita buat project nya dahulu
pnpm create svelte@latest sveltekit-todoapps
pilih skeleton project, typescript, checklist eslint, prettier dan playwright
cd sveltekit-todoapps
pnpm install
npx playwright install chromium
install packages
pnpm install drizzle-orm mysql2 bcrypt joi mysql2 uuid winston vite-node
pnpm install -D drizzle-kit @types/bcrypt @types/uuid
setup playwright config
sesuaikan isi playwright.config.ts seperti ini:
import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
workers: 1,
webServer: {
command: 'npm run dev:test',
port: 8000
},
testDir: 'tests',
testMatch: /(.+.)?(test|spec).[jt]s/
};
export default config;
adjust scripts
tambahkan scripts di package.json seperti ini:
"scripts": {
"dev:test": "vite dev -m test",
"migration:generate": "drizzle-kit generate:mysql",
"migration:push": "vite-node --options.transformMode.ssr='/.*/' src/lib/migrate.ts"
}
adjust vite config
adjust vite.config.ts seperti ini:
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
plugins: [sveltekit()],
server: {
port: Number(env.APP_PORT) || 5173,
host: env.APP_HOST || `localhost`,
https: false
}
};
});
adjust drizzle config
buat file drizzle.config.ts dengan isi seperti ini:
import type { Config } from 'drizzle-kit';
export default {
schema: 'src/lib/schema.ts',
out: 'drizzle'
} satisfies Config;
setup env files
buat file .env dan .env.example dengan isi seperti ini:
APP_URL=http://localhost
APP_HOST=localhost
APP_PORT=4173
DB_HOST=localhost
DB_DATABASE=todo_api
DB_USER=root
DB_PASS=''
DB_PORT=3306
buat file .env.test dengan isi seperti ini:
APP_URL=http://localhost
APP_HOST=localhost
APP_PORT=8000
DB_HOST=localhost
DB_DATABASE=todo_api_test
DB_USER=root
DB_PASS=''
DB_PORT=3306
Mulai
prepare database
buat file src/lib/database.ts
import { drizzle } from "drizzle-orm/mysql2";
import mysql from "mysql2/promise";
import { loadEnv } from "vite";
const env = loadEnv(import.meta.env?.MODE || 'test', process.cwd(), '')
const poolConnection = mysql.createPool({
host: env.DB_HOST,
user: env.DB_USER,
password: env.DB_PASSWORD,
database: env.DB_DATABASE,
port: Number(env.DB_PORT)
});
export const db = drizzle(poolConnection);
buat file src/lib/schema.ts
import { timestamp, customType, mysqlTable, varchar } from 'drizzle-orm/mysql-core';
// custom type unsigned big integer
const unsignedBigint = customType<{ data: number }>({
dataType() {
return 'bigint UNSIGNED';
}
});
// start
// custom type unsigned multiple of integer autoincrement primary key
type IdType = "tinyint" | "smallint" | "mediumint" | "int" | "bigint";
interface UIntConfig {
type?: IdType;
}
export const unsignedIntAutoIncrement = customType<{ data: number; config: UIntConfig; primaryKey: true; default: true }>({
dataType: (config) => {
return `${config?.type || "int"} UNSIGNED AUTO_INCREMENT`;
}
});
export function primary(dbName: string, config?: UIntConfig) {
return unsignedIntAutoIncrement(dbName, config).primaryKey()
};
// end
// Table users
export const users = mysqlTable('users', {
id: primary('id', { type: 'bigint' }),
name: varchar('username', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }).notNull().unique(),
password: varchar('password', { length: 255 }).notNull(),
token: varchar('token', { length: 255 }),
created_at: timestamp('created_at').defaultNow().notNull(),
updated_at: timestamp('updated_at').defaultNow().notNull().onUpdateNow()
});
// Table todos
export const todos = mysqlTable('todos', {
id: primary('id', { type: 'bigint' }),
user_id: unsignedBigint('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
title: varchar('title', { length: 255 }).notNull(),
description: varchar('description', { length: 255 }),
order: unsignedBigint('order').notNull(),
created_at: timestamp('created_at').defaultNow().notNull(),
updated_at: timestamp('updated_at').defaultNow().notNull().onUpdateNow()
});
buat file src/lib/migrate.ts
import { drizzle } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise';
import { loadEnv } from 'vite';
import { migrate } from 'drizzle-orm/mysql2/migrator';
import { askQuestion } from './util';
const ans = await askQuestion('input your environment: (default: development)') || 'development';
if (ans == '') process.exit(0)
const env = loadEnv(String(ans), process.cwd(), '');
const poolConnection = mysql.createPool({
host: env.DB_HOST,
user: env.DB_USER,
password: env.DB_PASSWORD,
database: env.DB_DATABASE,
port: Number(env.DB_PORT)
});
const db = drizzle(poolConnection);
async function main() {
console.log('migration start ..!');
await migrate(db, { migrationsFolder: "drizzle" });
console.log('migration success ..!');
process.exit(0);
}
await main().catch(console.error);
process.exit(0)
lalu jalankan
pnpm run migration:generate
pnpm run migration:push
prepare model
model user dan todo berikut hanyalah type untuk keperluan typescript
buat file src/lib/model/user.ts
export type user = {
id: number;
name: string;
email: string;
password?: string;
updated_at: Date;
created_at: Date;
};
dan file src/lib/model/todo.ts
export type todo = {
id: number;
user_id: number;
title: string;
description: string | null;
order: number;
created_at: Date;
updated_at: Date;
};
prepare factory
kita akan membuat userFactory dan todoFactory sebagai template factory ketika akan membuat fake user dan todo
buat file src/lib/factory/user.ts
import { db } from "../database";
import { users } from "../schema";
import bcrypt from 'bcrypt'
export async function userFactory(number: number, token?: string) {
for (let index = 1; index <= number; index++) {
const password = await bcrypt.hash("Password" + index, 10);
await db.insert(users).values({
name: "name" + index,
password: password,
token: token,
email: "email" + index + "@email.com",
}).execute();
}
}
buat file src/lib/factory/todo.ts
import { db } from "../database";
import { todos } from "../schema";
export async function todoFactory(number: number, userId: number) {
for (let index = 1; index <= number; index++) {
await db.insert(todos).values({
title: "title" + index,
description: "description" + index,
order: index,
user_id: userId,
}).execute();
}
}
prepare util.ts
buat file src/lib/util.ts
export function mysqlDatetimeUtc(date: Date = new Date()) {
return date.toISOString().slice(0, 19).replace('T', ' ');
}
// Use this function instead of new Date() when converting a MySQL datetime to a
// Date object so that the date is interpreted as UTC instead of local time (default behavior)
export function mysqlDatetimeUtcToDate(mysqlDatetimeUtc: string) {
return new Date(mysqlDatetimeUtc.replace(' ', 'T') + 'Z');
}
import readline from 'readline';
export function askQuestion(query: string) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) =>
rl.question(query, (ans) => {
rl.close();
resolve(ans);
})
);
}
prepare logger
buat file src/lib/logger.ts
import winston from 'winston';
export const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'app.log' })
]
});
buat file src/hooks.server.ts
import { db } from '$lib/database';
import { logger } from '$lib/logger';
import { users } from '$lib/schema';
import type { Handle } from '@sveltejs/kit';
import { error as responseError } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
import { eq } from 'drizzle-orm';
const authMiddleware: Handle = async ({ event, resolve }) => {
if (event.url.pathname.startsWith('/api/authenticated')) {
const token = event.request.clone().headers.get('authorization');
if (!token) {
throw responseError(401, {
message: 'unauthenticated'
});
}
const user = (await db.select().from(users).where(eq(users.token, token)).limit(1)).at(0);
if (!user) {
throw responseError(401, {
message: 'invalid token'
});
}
event.locals.user = user;
}
return await resolve(event)
}
const loggingMiddleware: Handle = async ({ event, resolve }) => {
const resp: Response = await resolve(event);
if (resp.status >= 400) {
logger.info(await resp.clone().json());
}
return resp;
}
export const handle: Handle = sequence(authMiddleware, loggingMiddleware);
membuat api register
buat file src/routes/register/+server.ts
import { db } from '$lib/database.js';
import { registerRequest } from '$lib/request/register.js';
import { users } from '$lib/schema.js';
import { json, error as responseError, type RequestHandler } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
import bcrypt from 'bcrypt';
export const POST: RequestHandler = async ({ request }) => {
const { name, email, password } = await request.json();
const { error } = registerRequest.validate({ name, email, password });
if (error) {
throw responseError(400, {
message: error.message
});
}
const isUserExist = await db.select().from(users).where(eq(users.email, email));
if (isUserExist.length > 0) {
throw responseError(400, {
message: 'user already exist'
});
}
const hashPassword = await bcrypt.hash(password, 10);
const user = await db.insert(users).values({
name: name,
email: email,
password: hashPassword
});
return json(
{
data: user,
message: 'register successfully',
code: 201
},
{ status: 201 }
);
}
buat registerRequest untuk validasi
buat file src/lib/request/register.ts
import Joi from 'joi';
export const registerRequest = Joi.object({
name: Joi.string().min(3).max(255).required(),
password: Joi.string().min(6).max(100).required(),
email: Joi.string().max(255).email().required()
});
buat userResource untuk mapping data yg akan di return di response api ini
buat file src/lib/resource/user.ts
import { mysqlDatetimeUtc } from "$lib/util";
import type { user } from "$lib/model/user";
type result = {
id: number;
name: string;
email: string;
updated_at: string;
created_at: string;
};
export const userResource = ( users: user[] ): result[] => {
const result: result[] = [];
users.forEach((user: user) => {
result.push({
id: user.id,
name: user.name,
email: user.email,
updated_at: mysqlDatetimeUtc(user.updated_at),
created_at: mysqlDatetimeUtc(user.created_at)
})
})
return result
};
buat unit test untuk api register
buat file tests/register.test.ts
import { sql } from 'drizzle-orm';
import { db } from '../src/lib/database';
import { test, expect } from '@playwright/test';
import { users } from '../src/lib/schema';
test.describe('test POST register api', () => {
test.beforeEach(async () => {
await db.execute(sql`set FOREIGN_KEY_CHECKS=0`);
await db.execute(sql`truncate users`);
await db.execute(sql`set FOREIGN_KEY_CHECKS=1`);
});
test('it can register user', async ({ request }) => {
const res = await request.post('/api/register', {
data : {
name: 'fajar sp',
email: '[email protected]',
password: 'Password123456'
}
});
const json = await res.json();
expect(res.status()).toBe(201);
expect(json.message).toEqual('register successfully');
});
test('it can validate exists user', async ({ request }) => {
await db.insert(users).values({
name: 'fajar sp',
email: '[email protected]',
password: 'Password123456'
})
const res = await request.post('/api/register', {
data: {
name: 'agung sp',
email: '[email protected]',
password: 'Password123456'
}
});
const json = await res.json();
expect(res.status()).toBe(400);
expect(json.message).toEqual('user already exist');
});
test('it can validate required field', async ({ request }) => {
const res = await request.post('/api/register', {
data: {
name: 'fajar sp',
email: '',
password: 'Password123456'
}
});
const json = await res.json();
expect(res.status()).toBe(400);
expect(json.message).toEqual('"email" is not allowed to be empty');
});
});
untuk menjalankan migration pada database testing run pnpm run migration:push
untuk melakukan test jalankan pnpm run test
membuat api login
buat file src/routes/login/+server.ts
import { db } from '$lib/database.js';
import { loginRequest } from '$lib/request/login.js';
import { users } from '$lib/schema.js';
import { json, error as responseError, type RequestHandler } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
import bcrypt from 'bcrypt';
import { v4 as uuidv4 } from 'uuid';
export const POST: RequestHandler = async ({ request }) => {
const { email, password } = await request.json();
const { error } = loginRequest.validate({ email, password });
if (error) {
throw responseError(400, {
message: error.message
});
}
const user = (await db.select().from(users).where(eq(users.email, email)).limit(1)).at(0);
if (!user || (await bcrypt.compare(password, user.password)) === false) {
throw responseError(400, {
message: 'email or password is invalid'
});
}
const token = uuidv4()
await db
.update(users)
.set({
token: token
})
.where(eq(users.id, user.id));
return json(
{
token: token,
message: 'login successfully',
code: 200
}
)
}
buat loginRequest untuk validasi
buat file src/lib/request/login.ts
import Joi from 'joi';
export const loginRequest = Joi.object({
email: Joi.string().max(255).email().required(),
password: Joi.string().min(6).max(100).required(),
});
buat unit test untuk api login
buat file tests/login.test.ts
import { sql } from 'drizzle-orm';
import { db } from '../src/lib/database';
import { test, expect } from '@playwright/test';
import { userFactory } from '../src/lib/factory/user';
test.describe('test POST login api', () => {
test.beforeEach(async () => {
await db.execute(sql`set FOREIGN_KEY_CHECKS=0`);
await db.execute(sql`truncate users`);
await db.execute(sql`set FOREIGN_KEY_CHECKS=1`);
await userFactory(1)
});
test('it can login', async ({ request }) => {
const res = await request.post('/api/login', {
data : {
email: '[email protected]',
password: 'Password1'
}
});
const json = await res.json();
expect(res.status()).toBe(200);
expect(json.token).not.toBeNull();
expect(json.message).toBe('login successfully');
});
test('it can validate wrong email', async ({ request }) => {
const res = await request.post('/api/login', {
data: {
email: '[email protected]',
password: 'Password1'
}
});
const json = await res.json();
expect(res.status()).toBe(400);
expect(json.message).toEqual('email or password is invalid');
});
test('it can validate wrong pass', async ({ request }) => {
const res = await request.post('/api/login', {
data: {
email: '[email protected]',
password: 'Password123456'
}
});
const json = await res.json();
expect(res.status()).toBe(400);
expect(json.message).toEqual('email or password is invalid');
});
});
buat api logout
buat file src/routes/authenticated/logout/+server.ts
import { db } from '$lib/database.js';
import { users } from '$lib/schema.js';
import { json, error as responsError, RequestHandler } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
export const DELETE: RequestHandler = async ({ locals }) => {
await db
.update(users)
.set({
token: null
})
.where(eq(users.id, locals.user.id))
.catch(err => {
throw responsError(400, {
message: err
})
});
return json({
data: locals.user,
message: 'Logged out successfully',
code: 200
});
}
buat test api logout
buat file tests/logout.test.ts
import { sql } from 'drizzle-orm';
import { db } from '../src/lib/database';
import { test, expect } from '@playwright/test';
import { userFactory } from '../src/lib/factory/user';
test.describe('test DELETE logout api', () => {
test.beforeEach(async () => {
await db.execute(sql`set FOREIGN_KEY_CHECKS=0`);
await db.execute(sql`truncate users`);
await db.execute(sql`set FOREIGN_KEY_CHECKS=1`);
await userFactory(1, 'mytoken');
});
test('it can logout', async ({ request }) => {
const res = await request.delete('/api/authenticated/logout', {
headers: {
authorization: 'mytoken'
}
});
const json = await res.json();
expect(res.status()).toBe(200);
expect(json.message).toBe('Logged out successfully');
});
});
buat test auth middleware
buat file tests/auth-middleware.test.ts
import { sql } from 'drizzle-orm';
import { db } from '../src/lib/database';
import { test, expect } from '@playwright/test';
import { userFactory } from '../src/lib/factory/user';
test.describe('test auth middleware', () => {
test.beforeEach(async () => {
await db.execute(sql`set FOREIGN_KEY_CHECKS=0`);
await db.execute(sql`truncate users`);
await db.execute(sql`set FOREIGN_KEY_CHECKS=1`);
await userFactory(1, 'mytoken');
});
test('it can validate with invalid token', async ({ request }) => {
const res = await request.post('/api/authenticated/myprofile', {
data: {
email: '[email protected]',
password: 'Password1'
},
headers: {
authorization: 'invalidtoken'
}
});
const json = await res.json();
expect(res.status()).toBe(401);
expect(json.message).toEqual('invalid token');
});
test('it can validate with no token', async ({ request }) => {
const res = await request.post('/api/authenticated/myprofile', {
data: {
email: '[email protected]',
password: 'Password1'
}
});
const json = await res.json();
expect(res.status()).toBe(401);
expect(json.message).toEqual('unauthenticated');
});
});
buat api myprofile
buat file src/routes/authenticated/myprofile/+server.ts
import { json, RequestHandler } from '@sveltejs/kit';
export const GET: RequestHandler = async ({ locals }) => {
return json({
data: locals.user,
message: 'get user profile successfully',
code: 200
});
}
buat test api myprofile
buat file tests/myprofile.test.ts
import { sql } from 'drizzle-orm';
import { db } from '../src/lib/database';
import { test, expect } from '@playwright/test';
import { userFactory } from '../src/lib/factory/user';
test.describe('test GET myprofile api', () => {
test.beforeEach(async () => {
await db.execute(sql`set FOREIGN_KEY_CHECKS=0`);
await db.execute(sql`truncate users`);
await db.execute(sql`set FOREIGN_KEY_CHECKS=1`);
await userFactory(1, 'mytoken');
});
test('it can get myprofile with valid token', async ({ request }) => {
const res = await request.get('/api/authenticated/myprofile', {
headers: {
authorization: 'mytoken'
}
});
const json = await res.json();
expect(res.status()).toBe(200);
expect(json.message).toBe('get user profile successfully');
});
});
buat api GET dan POST todos
buat file src/routes/authenticated/todos/+server.ts
import { db } from '$lib/database';
import { todoResource } from '$lib/resource/todo';
import { todos } from '$lib/schema';
import { json, type RequestHandler } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
import { error as responseError } from '@sveltejs/kit';
import { todoSaveRequest } from '$lib/request/todo';
export const GET = (async ({ locals, url }) => {
const size: number = url.searchParams.get('page[size]')
? Number(url.searchParams.get('page[size]'))
: 10;
const page: number = url.searchParams.get('page[number]')
? Number(url.searchParams.get('page[number]'))
: 1;
const offset = page > 0 ? (page - 1) * size : 0;
const data = await db
.select()
.from(todos)
.where(eq(todos.user_id, locals.user.id))
.limit(size)
.offset(offset);
return json(
{
message: 'get mytodo successfully',
data: todoResource(data),
code: 200,
meta: {
page: page,
size: size
}
},
{ status: 200 }
);
}) satisfies RequestHandler;
export const POST = (async ({ locals, request }) => {
const { title, description, order } = await request.json();
const { error } = todoSaveRequest.validate({ title, description, order });
if (error) {
throw responseError(400, {
message: error.message
});
}
const todo = await db.insert(todos).values({
title: title,
description: description,
order: order,
user_id: locals.user.id
});
return json(
{
data: todo,
message: 'Todo added successfully',
code: 201
},
{ status: 201 }
);
}) satisfies RequestHandler;
buat todoSaveRequest untuk validasi
buat file src/lib/request/todo.ts
import Joi from 'joi';
export const todoSaveRequest = Joi.object({
title: Joi.string().min(3).max(255).required(),
description: Joi.string().max(255).not().required(),
order: Joi.number().required()
});
buat todoResource untuk mapping data yg akan di return di response api ini
buat file src/lib/resource/todo.ts
import { mysqlDatetimeUtc } from "$lib/util";
import type { todo } from "$lib/model/todo";
type result = {
id: number;
title: string;
description: string;
order: number;
updated_at: string;
created_at: string;
};
export const todoResource = ( todos: todo[] ): result[] => {
const result: result[] = [];
todos.forEach((todo: todo) => {
result.push({
id: todo.id,
title: todo.title,
description: todo.description || '',
order: todo.order,
updated_at: mysqlDatetimeUtc(todo.updated_at),
created_at: mysqlDatetimeUtc(todo.created_at)
});
})
return result
};
buat api PUT dan DELETE todos
buat file src/routes/authenticated/todos/[id]/+server.ts
import { db } from '$lib/database';
import { todos } from '$lib/schema';
import { json, type RequestHandler } from '@sveltejs/kit';
import { and, eq } from 'drizzle-orm';
import { error as responseError } from '@sveltejs/kit';
import { todoSaveRequest } from '$lib/request/todo';
export const PUT = (async ({ params, locals, request }) => {
const { title, description, order } = await request.json();
const { error } = todoSaveRequest.validate({ title, description, order });
if (error) {
throw responseError(400, {
message: error.message
});
}
const mytodo = (
await db
.select()
.from(todos)
.where(eq(todos.user_id, locals.user.id))
.where(eq(todos.id, Number(params.id)))
.limit(1)
).at(0);
if (!mytodo) {
throw responseError(404, {
message: 'Todo not found'
});
}
await db
.update(todos)
.set({
title: title,
description: description,
order: order
})
.where(eq(todos.user_id, locals.user.id))
.where(eq(todos.id, Number(mytodo.id)))
.catch((err) => {
throw responseError(400, {
message: err
});
});
return json({
message: `Todo ${mytodo.id} updated successfully`,
code: 200
});
}) satisfies RequestHandler;
export const DELETE = (async ({ locals, params }) => {
const mytodo = (
await db
.select()
.from(todos)
.where(and(eq(todos.user_id, locals.user.id), eq(todos.id, Number(params.id))))
.limit(1)
).at(0);
if (!mytodo) {
throw responseError(404, {
message: 'Todo not found'
});
}
await db
.delete(todos)
.where(and(eq(todos.user_id, locals.user.id), eq(todos.id, Number(params.id))))
.catch((err) => {
throw responseError(400, {
message: err
});
});
return json({
data: mytodo,
message: `Todo ${mytodo.id} deleted successfully`,
code: 200
});
}) satisfies RequestHandler;
buat test api todos
buat file tests/todos.test.ts
import { sql } from 'drizzle-orm';
import { db } from '../src/lib/database';
import { test, expect } from '@playwright/test';
import { userFactory } from '../src/lib/factory/user';
import { todoFactory } from '../src/lib/factory/todo';
test.describe('test todo api', () => {
test.beforeEach(async () => {
await db.execute(sql`set FOREIGN_KEY_CHECKS=0`);
await db.execute(sql`truncate users`);
await db.execute(sql`truncate todos`);
await db.execute(sql`set FOREIGN_KEY_CHECKS=1`);
await userFactory(1, 'mytoken');
});
test('it can add new todo', async ({ request }) => {
const res = await request.post('/api/authenticated/todos', {
headers: {
authorization: 'mytoken'
},
data: {
title: 'title',
description: 'description',
order: 1
}
});
const json = await res.json();
expect(res.status()).toBe(201);
expect(json.message).toBe('Todo added successfully');
});
test('it can get my todo', async ({ request }) => {
await todoFactory(2, 1);
const res = await request.get('/api/authenticated/todos', {
headers: {
authorization: 'mytoken'
}
});
const json = await res.json();
expect(res.status()).toBe(200);
expect(json.message).toBe('get mytodo successfully');
});
test('it can update my todo', async ({ request }) => {
await todoFactory(2, 1);
const res = await request.put('/api/authenticated/todos/1', {
headers: {
authorization: 'mytoken'
},
data: {
title: 'updated title',
description: 'description',
order: 1
}
});
const json = await res.json();
expect(res.status()).toBe(200);
expect(json.message).toBe(`Todo 1 updated successfully`);
});
test('it can delete my todo', async ({ request }) => {
await todoFactory(2, 1);
const res = await request.delete('/api/authenticated/todos/1', {
headers: {
authorization: 'mytoken'
}
});
const json = await res.json();
expect(res.status()).toBe(200);
expect(json.message).toBe(`Todo 1 deleted successfully`);
});
});
source code
untuk full source code bisa cek disini https://github.com/jhonoryza/svelkit-todoapps-api
