diego-aquino / zimic Goto Github PK
View Code? Open in Web Editor NEWNext-gen, TypeScript-first HTTP request mocking
Home Page: https://www.npmjs.com/package/zimic
License: MIT License
Next-gen, TypeScript-first HTTP request mocking
Home Page: https://www.npmjs.com/package/zimic
License: MIT License
Marking request trackers as unusable after the interceptor is cleared makes sure no outdated interceptors are used. However, this strategy makes it impossible to reuse interceptors between tests:
const userListTracker = userInterceptor.get('/users')
beforeEach(async () => {
userInterceptor.clear()
})
it('test 1', () => {
userListTracker.respond(...) // won't work
})
it('test 2', () => {
userListTracker.respond(...) // won't work
})
const creationTracker = authInterceptor
.post('/users')
.with({
formData: creationPayload,
body: blob, // or File or Buffer
})
.respond({
status: 201,
body: user,
});
zimic-test-client
using one worker and multiple interceptorszimic-test-client
using multiple workers (there should be an error)Zimic currently only applies HTTP mocks to the process where the interceptor is called. This makes it unusable in multi-process scenarios.
An example that illustrates this case:
next dev
.localhost
.This is a great use case for Zimic, since mocking external applications with type safety is one of our goals. However, this scenario is not currently supported. For example, a test of this application could look like:
A feature that would extend Zimic to this use case would be the introduction of "remote HTTP interceptors". Remote interceptors are similar to our current interceptors, but they apply mocks to remote processes instead of the one the interceptor is initialized.
Using the example above, the workflow using remote interceptors would be like:
/zimic
), available only in development and testing. This endpoint would be used by Zimic to receive mocks definitions and apply them to the Next.js process.// creates a RemoteHttpInterceptor
const remoteInterceptor = createHttpInterceptor<{ ... }>({
type: 'remote' // 'local' is the default
remoteURL: 'http://localhost:3000/zimic',
})
const getTracker = await remoteInterceptor.get('/').respond({
status: 200,
body: { success: true }
})
// mock applied to remote application
const getRequests = await getTracker.requests()
expect(getRequests).toHaveLenght(1)
A feature that would extend Zimic to this use case would be the introduction of remote HTTP interceptors. Remote interceptors are similar to our current interceptors, but they apply mocks to a remote Zimic server process, instead of the process where the interceptor is initialized.
Using the example above, the workflow using remote interceptors would be like:
type InterceptorSchema = HttpInterceptorSchema.Root<{
'/': {
GET: {
response: {
200: { body: { success: true } };
};
};
};
}>;
const localWorker = createWorker({
type: 'local',
});
await localWorker.start(); // starts the local worker, applies the mocks to the local process
// creates an HttpInterceptor
const localInterceptor = createHttpInterceptor<InterceptorSchema>({
worker: localWorker,
baseURL: 'http://localhost:3000',
});
// applying remote mocks is a asynchronous operation
const getTracker = localInterceptor.get('/').respond({
status: 200,
body: { success: true },
});
// mock applied to local process
const getRequests = getTracker.requests();
expect(getRequests).toHaveLength(1);
type InterceptorSchema = HttpInterceptorSchema.Root<{
'/': {
GET: {
response: {
200: { body: { success: true } };
};
};
};
}>;
const remoteWorker = createWorker({
type: 'remote',
serverURL: 'http://localhost:3001', // the URL of the zimic mock server
});
// start the zimic mock server with:
// zimic server start --port 3001 --on-ready 'npm test' --ephemeral true
await remoteWorker.start(); // starts the remote worker and links it to the zimic mock server
// creates a RemoteHttpInterceptor
const remoteInterceptor = createHttpInterceptor<InterceptorSchema>({
worker: remoteWorker,
// the application should use the base URL of the external service as http://localhost:3001/service1
pathPrefix: '/service1',
});
// applying remote mocks is an asynchronous operation
const getTracker = await remoteInterceptor.get('/').respond({
status: 200,
body: { success: true },
});
// mock applied to remote server:
// any requests to http://localhost:3001 will be handled by the zimic mock server with the mock applied
// getting requests is also an asynchronous operation
const getRequests = await getTracker.requests();
expect(getRequests).toHaveLength(1);
zimic
main packageDiscussion: #75
Consider removing the platform
parameter from worker factories. We can infer the platform automatically.
mockServerURL
to serverURL
ZIMIC_SERVER_PORT
in the test command of zimic-test-client
zimic server start --port 3000 --ephemeral -- pnpm test
local
to remote
(users have to add await to every mock applied)
README.md
expectNoUnusedHandlers(): void;
afterAll(() => {
authInterceptor.expectNoUnusedHandlers();
});
const getTracker = authInterceptor
.get('/users/:id')
.with({ pathParams: { id: user.id } })
.respond({
status: 200,
body: user,
});
// Code to test
const response = await getUserById(user.id);
expect(response.status).toBe(200);
const returnedUsers = (await response.json()) as User[];
expect(returnedUsers).toEqual([user]);
const getRequests = getTracker.requests();
expect(getRequests).toHaveLength(1);
// Simpler alternative:
// expect(getRequests[0].routeParams).toEqual({ id: user.id });
expect(getRequests[0].routeParams.id).toBe(user.id);
const getTracker = authInterceptor.get(`/users/${user.id}`).respond({
status: 200,
body: user,
});
// Code to test
const response = await getUserById(user.id);
expect(response.status).toBe(200);
const returnedUsers = (await response.json()) as User[];
expect(returnedUsers).toEqual([user]);
const getRequests = getTracker.requests();
expect(getRequests).toHaveLength(1);
// Simpler alternative:
// expect(getRequests[0].routeParams).toEqual({});
expect(getRequests[0].routeParams.id).toBe(undefined);
const listTracker = authInterceptor
.get('/users')
.with({
headers: { 'content-type': 'application/json' },
exact: true,
})
.respond({
status: 200,
body: [user],
});
README.md
/app
, tested with playwright/pages
, tested with playwrightREADME.md
Hi, @diego-aquino
I'm trying to use zimic with supabase.
const supabaseInterceptor = createHttpInterceptor<{
'/auth/v1/admin/users': {
POST: {
request: {
body: { email: '[email protected]'; password: '123456' }
}
response: {
200: { body: { user: { id: '123' } } }
}
}
}
}>({
worker,
baseURL: 'http://localhost:8000'
})
My supabase environtment:
SUPABASE_URL=http://localhost:8000
node: 20.10.0
honojs: 4.0.0
ConnectionRefused: Unable to connect. Is the computer able to access the url?
path: "http://localhost:8000/auth/v1/admin/users"
Keep going! amazing work here!
v0.1.0-canary.0
)v0.1.0
)createHttpInterceptor
clearHandlers: (options?: { includeDefaults?: boolean }) => void;
createHttpInterceptor
Support to body, search params, route params and form data will be added in the future.
const creationTracker = authInterceptor
.post('/users')
.with({
body: creationPayload,
}, { exact: true })
.respond({
status: 201,
body: user,
});
> pnpm zimic browser init public
[zimic] Copying the service worker script to ~/zimic/apps/zimic-test-client/public...
cli.js browser init <publicDirectory>
Initialize the browser service worker
Positionals:
publicDirectory The path to the public directory of your application
[string] [required]
Options:
--help Show help [boolean]
--version Show version number [boolean]
[Error: ENOENT: no such file or directory, copyfile '~/zimic/node_modules/.pnpm/[email protected][email protected]/node_modules/zimic/node_modules/msw/lib/mockServiceWorker.js' -> '~/www/zimic/apps/zimic-test-client/public/mockServiceWorker.js'] {
errno: -2,
code: 'ENOENT',
syscall: 'copyfile',
path: '~/www/zimic/node_modules/.pnpm/[email protected][email protected]/node_modules/zimic/node_modules/msw/lib/mockServiceWorker.js',
dest: '~/www/zimic/apps/zimic-test-client/public/mockServiceWorker.js'
}
Following the discussions on #21, Zimic will support inferring and generating interceptor type schema from OpenAPI/Swagger specifications. Learn more at the section "Alternative 3" of the summary comment.
const listTracker = authInterceptor
.get('/users')
.with({
searchParams: { name: nameToFilter },
exact: true
})
.respond({
status: 200,
body: [user],
});
'ignore' | 'warn'
or a function receiving a request. The default is 'warn'
.'ignore'
and 'warn'
, output structured logs containing all relevant fields of the request, to help developers know which specific requests were not matched. For example:
[zimic] Request did not match any filters: Request { headers: { 'content-type': 'application/json' }, body: 'message' }
import * as crypto from 'crypto';
import { createHttpInterceptor } from './lib';
import { describe, beforeEach, it, expect, afterAll } from '../common/test';
interface User {
id: string;
name: string;
email: string;
}
interface UserWithPassword extends User {
password: string;
}
type UserCreationPayload = Omit<UserWithPassword, 'id'>;
interface LoginResult {
accessToken: string;
refreshToken: string;
}
interface RequestError {
code: string;
message: string;
}
interface ValidationError extends RequestError {
code: 'validation_error';
}
interface UnauthorizedError extends RequestError {
code: 'unauthorized';
}
interface NotFoundError extends RequestError {
code: 'not_found';
}
interface ConflictError extends RequestError {
code: 'conflict';
}
const authInterceptor = createHttpInterceptor<{
'/users': {
POST: {
request: {
body: UserCreationPayload;
};
response: {
201: { body: User };
400: { body: ValidationError };
409: { body: ConflictError };
};
};
GET: {
request: {
searchParams: {
name?: string;
email?: string;
};
};
response: {
200: { body: User[] };
};
};
};
'/users/:id': {
GET: {
request: {
routeParams: { id: string };
};
response: {
200: { body: User };
404: { body: NotFoundError };
};
};
PATCH: {
request: {
headers: { authorization: string };
routeParams: { id: string };
body: Partial<User>;
};
response: {
200: { body: User };
404: { body: NotFoundError };
};
};
DELETE: {
request: {
headers: { authorization: string };
routeParams: { id: string };
};
response: {
204: { body: void };
404: { body: NotFoundError };
};
};
};
'/session/login': {
POST: {
request: {
body: {
email: string;
password: string;
};
};
response: {
201: { body: LoginResult };
400: { body: ValidationError };
401: { body: UnauthorizedError };
};
};
};
'/session/refresh': {
POST: {
request: {
body: { refreshToken: string };
};
response: {
201: { body: LoginResult };
401: { body: UnauthorizedError };
};
};
};
'/session/logout': {
POST: {
request: {
headers: { authorization: string };
};
response: {
204: { body: void };
401: { body: UnauthorizedError };
};
};
};
}>({
baseURL: 'https://localhost:3000',
unhandledRequestStrategy: 'error',
defaults: {
'/users': {
GET: {
response: {
status: 200,
body: [],
},
},
},
'/users/:id': {
GET: {
response: {
status: 404,
body: {
code: 'not_found',
message: 'User not found',
},
},
},
},
},
});
describe('Users', () => {
const user: User = {
id: crypto.randomUUID(),
name: 'Name',
email: '[email protected]',
};
beforeEach(() => {
authInterceptor.clearHandlers();
});
afterAll(() => {
authInterceptor.expectNoUnusedHandlers();
});
describe('User creation', () => {
const creationPayload: UserCreationPayload = {
name: user.name,
email: user.email,
password: 'password',
};
async function createUser(payload: UserCreationPayload) {
return await fetch('https://localhost:3000/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
}
it('should support creating users - alternative 1', async () => {
const creationTracker = authInterceptor
.post('/users')
.with({
body: creationPayload,
})
.respond({
status: 201,
body: user,
});
// Code to test
const response = await createUser(creationPayload);
expect(response.status).toBe(201);
const createdUser = (await response.json()) as User;
expect(createdUser).toEqual(user);
const creationRequests = creationTracker.requests();
expect(creationRequests).toHaveLength(1);
// Simpler alternative:
// expect(creationRequests[0].body).toEqual(creationPayload);
expect(creationRequests[0].body.name).toBe(creationPayload.name);
expect(creationRequests[0].body.email).toBe(creationPayload.email);
expect(creationRequests[0].body.password).toBe(creationPayload.password);
});
it('should support creating users - alternative 2', async () => {
const creationTracker = authInterceptor.post('/users').respond({
status: 201,
body: user,
});
// Code to test
const response = await createUser(creationPayload);
expect(response.status).toBe(201);
const createdUser = (await response.json()) as User;
expect(createdUser).toEqual(user);
const creationRequests = creationTracker.requests();
expect(creationRequests).toHaveLength(1);
// Simpler alternative:
// expect(creationRequests[0].body).toEqual(creationPayload);
expect(creationRequests[0].body.name).toBe(creationPayload.name);
expect(creationRequests[0].body.email).toBe(creationPayload.email);
expect(creationRequests[0].body.password).toBe(creationPayload.password);
});
it('should return an error if the payload is not valid', async () => {
// @ts-expect-error Forcing an invalid payload
const invalidPayload: UserCreationPayload = {};
const creationTracker = authInterceptor
.post('/users')
.with({
body: invalidPayload,
})
.respond({
status: 400,
body: {
code: 'validation_error',
message: 'Invalid payload',
},
});
// Code to test
const response = await createUser(invalidPayload);
expect(response.status).toBe(400);
const creationRequests = creationTracker.requests();
expect(creationRequests).toHaveLength(1);
expect(creationRequests[0].body).toEqual(invalidPayload);
});
it('should return an error if the payload is not valid', async () => {
const conflictingPayload: UserCreationPayload = creationPayload;
const creationTracker = authInterceptor
.post('/users')
.with({
body: conflictingPayload,
})
.respond({
status: 409,
body: {
code: 'conflict',
message: 'User already exists',
},
});
// Code to test
const response = await createUser(conflictingPayload);
expect(response.status).toBe(409);
const creationRequests = creationTracker.requests();
expect(creationRequests).toHaveLength(1);
expect(creationRequests[0].body).toEqual(conflictingPayload);
});
});
describe('User list', () => {
async function listUsers(filters: { name?: string; email?: string } = {}) {
const searchParams = new URLSearchParams(filters);
return await fetch(`https://localhost:3000/users?${searchParams}`, { method: 'GET' });
}
it('should list users filtered by name', async () => {
const nameToFilter = 'nam';
const listTracker = authInterceptor
.get('/users')
.with({
searchParams: { name: nameToFilter },
})
.respond({
status: 200,
body: [user],
});
// Code to test
const response = await listUsers({ name: nameToFilter });
expect(response.status).toBe(200);
const returnedUsers = (await response.json()) as User[];
expect(returnedUsers).toEqual([user]);
const listRequests = listTracker.requests();
expect(listRequests).toHaveLength(1);
// Simpler alternative:
// expect(listInterceptions[0].searchParams).toEqual(
// new URLSearchParams({
// name: nameToFilter,
// }),
// );
expect(listRequests[0].searchParams.size).toBe(1);
expect(listRequests[0].searchParams.get('name')).toBe(nameToFilter);
});
});
describe('User get by id', () => {
async function getUserById(userId: string) {
return await fetch(`https://localhost:3000/users/${userId}`, { method: 'GET' });
}
it('should support getting users by id - alternative 1', async () => {
const getTracker = authInterceptor
.get('/users/:id')
.with({ routeParams: { id: user.id } })
.respond({
status: 200,
body: user,
});
// Code to test
const response = await getUserById(user.id);
expect(response.status).toBe(200);
const returnedUsers = (await response.json()) as User[];
expect(returnedUsers).toEqual([user]);
const getRequests = getTracker.requests();
expect(getRequests).toHaveLength(1);
// Simpler alternative:
// expect(getRequests[0].routeParams).toEqual({ id: user.id });
expect(getRequests[0].routeParams.id).toBe(user.id);
});
it('should support getting users by id - alternative 2', async () => {
const getTracker = authInterceptor.get<'/users/:id'>(`/users/${user.id}`).respond({
status: 200,
body: user,
});
// Code to test
const response = await getUserById(user.id);
expect(response.status).toBe(200);
const returnedUsers = (await response.json()) as User[];
expect(returnedUsers).toEqual([user]);
const getRequests = getTracker.requests();
expect(getRequests).toHaveLength(1);
// Simpler alternative:
// expect(getRequests[0].routeParams).toEqual({});
expect(getRequests[0].routeParams.id).toBe(undefined);
});
it('should return an error if the user was not found', async () => {
const getTracker = authInterceptor
.get('/users/:id')
.with({
routeParams: { id: user.id },
})
.respond({
status: 404,
body: {
code: 'not_found',
message: 'User not found',
},
});
// Code to test
const response = await getUserById(user.id);
expect(response.status).toBe(404);
const getRequests = getTracker.requests();
expect(getRequests).toHaveLength(1);
// Simpler alternative:
// expect(getRequests[0].routeParams).toEqual({});
expect(getRequests[0].routeParams.id).toBe(user.id);
});
});
describe('User deletion', () => {
async function deleteUserById(userId: string) {
return await fetch(`https://localhost:3000/users/${userId}`, { method: 'DELETE' });
}
it('should support deleting users by id', async () => {
const deleteTracker = authInterceptor
.delete('/users/:id')
.with({ routeParams: { id: user.id } })
.respond({
status: 204,
});
// Code to test
const response = await deleteUserById(user.id);
expect(response.status).toBe(204);
const deleteRequests = deleteTracker.requests();
expect(deleteRequests).toHaveLength(1);
// Simpler alternative:
// expect(deleteRequests[0].routeParams).toEqual({ id: user.id });
expect(deleteRequests[0].routeParams.id).toBe(user.id);
});
it('should return an error if the user was not found', async () => {
const getTracker = authInterceptor
.delete('/users/:id')
.with({
routeParams: { id: user.id },
})
.respond({
status: 404,
body: {
code: 'not_found',
message: 'User not found',
},
});
// Code to test
const response = await deleteUserById(user.id);
expect(response.status).toBe(404);
const getRequests = getTracker.requests();
expect(getRequests).toHaveLength(1);
// Simpler alternative:
// expect(getRequests[0].routeParams).toEqual({});
expect(getRequests[0].routeParams.id).toBe(user.id);
});
});
});
import * as crypto from 'crypto';
import { createHttpInterceptor } from './lib';
import { describe, beforeEach, it, expect, afterAll } from '../common/test';
interface User {
id: string;
name: string;
email: string;
}
interface UserWithPassword extends User {
password: string;
}
type UserCreationPayload = Omit<UserWithPassword, 'id'>;
interface RequestError {
code: string;
message: string;
}
interface ValidationError extends RequestError {
code: 'validation_error';
}
interface ConflictError extends RequestError {
code: 'conflict';
}
const authInterceptor = createHttpInterceptor<{
'/users': {
POST: {
request: {
body: UserCreationPayload;
};
response: {
201: { body: User };
400: { body: ValidationError };
409: { body: ConflictError };
};
};
};
}>({
baseURL: 'https://localhost:3000',
unhandledRequestStrategy: 'error',
defaults: {
'/users': {
POST: {
response: {
status: 400,
body: {
code: 'validation_error',
message: 'Invalid payload',
},
},
},
},
},
});
const user: User = {
id: crypto.randomUUID(),
name: 'Name',
email: '[email protected]',
};
const creationPayload: UserCreationPayload = {
name: user.name,
email: user.email,
password: 'password',
};
async function createUser(payload: UserCreationPayload) {
return await fetch('https://localhost:3000/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
}
describe('Users', () => {
beforeEach(() => {
authInterceptor.clearHandlers();
});
afterAll(() => {
authInterceptor.expectNoUnusedHandlers();
});
describe('User creation', () => {
it('should support creating users', async () => {
const creationTracker = authInterceptor
.post('/users')
.with({
body: creationPayload,
})
.respond({
status: 201,
body: user,
});
// Code to test
const response = await createUser(creationPayload);
expect(response.status).toBe(201);
const creationRequests = creationTracker.requests();
expect(creationRequests).toHaveLength(1);
expect(creationRequests[0].body.name).toBe(creationPayload.name);
expect(creationRequests[0].body.email).toBe(creationPayload.email);
expect(creationRequests[0].body.password).toBe(creationPayload.password);
});
});
});
import * as crypto from 'crypto';
import { createHttpInterceptor } from './lib';
import { describe, beforeEach, it, expect, afterAll } from '../common/test';
export interface UserCreationPayload {
name: string;
email: string;
password: string;
}
export interface User {
id: string;
name: string;
email: string;
}
export interface ValidationErrorResponse {
code: 'validation_error';
message: string;
}
export const authInterceptor = createHttpInterceptor<{
'/users': {
POST: {
request: {
body: UserCreationPayload;
};
response: {
201: { body: User };
400: { body: ValidationErrorResponse };
};
};
};
}>({
baseURL: 'http://localhost:3000',
unhandledRequestStrategy: 'error',
defaults: {
'/users': {
POST: {
response: {
status: 400,
body: {
code: 'validation_error',
message: 'Validation error',
},
},
},
},
},
});
async function createUser(payload: UserCreationPayload) {
return fetch('https://localhost:3000/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
}
describe('Users', () => {
const user: User = {
id: crypto.randomUUID(),
name: 'Name',
email: '[email protected]',
};
beforeEach(() => {
authInterceptor.clearHandlers();
});
afterAll(() => {
authInterceptor.expectNoUnusedHandlers();
});
describe('User creation', () => {
it('should support creating users', async () => {
const creationPayload: UserCreationPayload = {
name: user.name,
email: user.email,
password: 'password',
};
const creationTracker = authInterceptor
.post('/users')
.with({
body: creationPayload,
})
.respond({
status: 201,
body: user,
});
// Code to test
const response = await createUser(creationPayload);
expect(response.status).toBe(201);
const creationRequests = creationTracker.requests();
expect(creationRequests).toHaveLength(1);
expect(creationRequests[0].body.name).toBe(creationPayload.name);
expect(creationRequests[0].body.email).toBe(creationPayload.email);
expect(creationRequests[0].body.password).toBe(creationPayload.password);
});
});
});
import * as crypto from 'crypto';
import { createHttpInterceptor } from './lib';
import { describe, beforeEach, it, expect, afterAll } from '../common/test';
interface User {
id: string;
name: string;
email: string;
}
interface UserWithPassword extends User {
password: string;
}
type UserCreationPayload = Omit<UserWithPassword, 'id'>;
interface RequestError {
code: string;
message: string;
}
interface ValidationError extends RequestError {
code: 'validation_error';
}
interface ConflictError extends RequestError {
code: 'conflict';
}
const authInterceptor = createHttpInterceptor<{
'/users': {
POST: {
request: {
formData: UserCreationPayload;
};
response: {
201: { body: User };
400: { body: ValidationError };
409: { body: ConflictError };
};
};
};
}>({
baseURL: 'https://localhost:3000',
unhandledRequestStrategy: 'error',
});
const user: User = {
id: crypto.randomUUID(),
name: 'Name',
email: '[email protected]',
};
const creationPayload: UserCreationPayload = {
name: user.name,
email: user.email,
password: 'password',
};
async function createUser(payload: UserCreationPayload) {
return await fetch('https://localhost:3000/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
}
describe('Users', () => {
beforeEach(() => {
authInterceptor.clearHandlers();
});
afterAll(() => {
authInterceptor.expectNoUnusedHandlers();
});
describe('User creation', () => {
it('should support creating users', async () => {
const creationTracker = authInterceptor
.post('/users')
.with({
formData: creationPayload,
})
.respond({
status: 201,
body: user,
});
// Code to test
const response = await createUser(creationPayload);
expect(response.status).toBe(201);
const creationRequests = creationTracker.requests();
expect(creationRequests).toHaveLength(1);
expect(creationRequests[0].formData.get('name')).toBe(creationPayload.name);
expect(creationRequests[0].formData.get('email')).toBe(creationPayload.email);
expect(creationRequests[0].formData.get('password')).toBe(creationPayload.password);
});
});
});
Utility package to be installed in client apps, supporting schema typegen generation by CLI
Utility package to be installed in client apps, supporting schema typegen generation by using the app
Runtime endpoint specification
Assessment: promising, not feasible directly.
A utility package could be installed in a mocked service, allowing automatic schema type generation by a CLI. The generated types are exported to an intermediate package that must be imported by client apps.
Workflow:
Pros:
Cons:
Assessment: not promising, requires manual intervention and is error prone.
A utility package could be installed in client apps, allowing automatic schema type generation by inferring the types from real requests.
Workflow:
Pros:
Cons:
Assessment: most promising, will be implemented into Zimic.
Having access to the mocked service endpoint specification, Zimic could automatically generate the type schemas. The endpoint specification could follow the standard OpenAPI.
Workflow:
Pros:
Cons:
There could be ways to infer the OpenAPI specification from the source code of the mocked service. By doing this, alternatives 1 and 3 are combined:
See:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.