Skip to content

Commit 73bb100

Browse files
committed
Improve API docs with examples, CORS, and move to /api
- Move API docs from /api-docs to /api for cleaner URL - Add CORS headers and X-Execution-Time to /api/jq responses - Add OpenAPI examples for all endpoints so users can test immediately - Add query params schema for GET /api/jq - Add path params schema for GET /api/snippets/{slug} - Create post-processing script to inject examples after generation
1 parent 8766366 commit 73bb100

File tree

9 files changed

+247
-21
lines changed

9 files changed

+247
-21
lines changed

next.openapi.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"info": {
44
"title": "jq Playground API",
55
"version": "1.0.0",
6-
"description": "Execute jq queries and share snippets"
6+
"description": "Execute jq queries and share snippets.\n\n**Want a UI?** Try the [jq playground](https://play.jqlang.org) for an interactive experience."
77
},
88
"servers": [
99
{

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"copy-monaco": "bash scripts/copy-monaco.sh",
1414
"setup-db": "node scripts/setup-db.js",
1515
"prisma:migrate": "npm run setup-db && npx prisma migrate dev",
16-
"openapi": "next-openapi-gen generate"
16+
"openapi": "next-openapi-gen generate && node scripts/add-openapi-examples.js"
1717
},
1818
"dependencies": {
1919
"@emotion/react": "^11.14.0",

public/openapi.json

Lines changed: 104 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"info": {
44
"title": "jq Playground API",
55
"version": "1.0.0",
6-
"description": "Execute jq queries and share snippets"
6+
"description": "Execute jq queries and share snippets.\n\n**Want a UI?** Try the [jq playground](https://play.jqlang.org) for an interactive experience."
77
},
88
"servers": [
99
{
@@ -32,6 +32,31 @@
3232
"status"
3333
]
3434
},
35+
"JqQueryParamsSchema": {
36+
"type": "object",
37+
"properties": {
38+
"json": {
39+
"type": "string",
40+
"description": "JSON input to process",
41+
"example": "{\"name\": \"jq\", \"version\": \"1.7\"}"
42+
},
43+
"query": {
44+
"type": "string",
45+
"description": "jq query to execute",
46+
"example": ".name"
47+
},
48+
"options": {
49+
"type": "string",
50+
"nullable": true,
51+
"description": "Comma-separated jq options (e.g., \"-r,-c\")",
52+
"example": "-r,-c"
53+
}
54+
},
55+
"required": [
56+
"json",
57+
"query"
58+
]
59+
},
3560
"JqResponseSchema": {
3661
"type": "object",
3762
"properties": {
@@ -49,28 +74,43 @@
4974
"properties": {
5075
"json": {
5176
"type": "string",
52-
"description": "JSON input to process"
77+
"description": "JSON input to process",
78+
"example": "{\"name\": \"jq\", \"version\": \"1.7\"}"
5379
},
5480
"query": {
5581
"type": "string",
56-
"description": "jq query to execute"
82+
"description": "jq query to execute",
83+
"example": ".name"
5784
},
5885
"options": {
5986
"allOf": [
6087
{
6188
"$ref": "#/components/schemas/Options"
6289
}
6390
],
64-
"description": "jq command-line options"
91+
"description": "jq command-line options",
92+
"example": [
93+
"-r",
94+
"-c"
95+
]
6596
}
6697
},
6798
"required": [
6899
"json",
69100
"query"
70101
]
71102
},
72-
"slug": {
73-
"type": "object"
103+
"SnippetPathParamsSchema": {
104+
"type": "object",
105+
"properties": {
106+
"slug": {
107+
"type": "string",
108+
"description": "Unique snippet identifier"
109+
}
110+
},
111+
"required": [
112+
"slug"
113+
]
74114
},
75115
"Snippet": {
76116
"type": "object",
@@ -401,11 +441,45 @@
401441
"get": {
402442
"operationId": "get-jq",
403443
"summary": "Execute a jq query via query parameters",
404-
"description": "Execute a jq query against JSON input via query parameters",
444+
"description": "Execute a jq query against JSON input via query parameters. Great for simple queries and quick testing.",
405445
"tags": [
406446
"Jq"
407447
],
408-
"parameters": [],
448+
"parameters": [
449+
{
450+
"in": "query",
451+
"name": "json",
452+
"schema": {
453+
"type": "string",
454+
"description": "JSON input to process",
455+
"example": "{\"name\": \"jq\", \"version\": \"1.7\"}"
456+
},
457+
"required": false,
458+
"description": "JSON input to process"
459+
},
460+
{
461+
"in": "query",
462+
"name": "query",
463+
"schema": {
464+
"type": "string",
465+
"description": "jq query to execute",
466+
"example": ".name"
467+
},
468+
"required": false,
469+
"description": "jq query to execute"
470+
},
471+
{
472+
"in": "query",
473+
"name": "options",
474+
"schema": {
475+
"type": "string",
476+
"description": "Comma-separated jq options (e.g., \"-r,-c\")",
477+
"example": "-r,-c"
478+
},
479+
"required": false,
480+
"description": "Comma-separated jq options (e.g., \"-r,-c\")"
481+
}
482+
],
409483
"responses": {
410484
"200": {
411485
"description": "Successful response",
@@ -428,7 +502,7 @@
428502
"post": {
429503
"operationId": "post-jq",
430504
"summary": "Execute a jq query",
431-
"description": "Execute a jq query against JSON input",
505+
"description": "Execute a jq query against JSON input. Use this for complex queries or large JSON payloads.",
432506
"tags": [
433507
"Jq"
434508
],
@@ -476,6 +550,13 @@
476550
"application/json": {
477551
"schema": {
478552
"$ref": "#/components/schemas/Snippet"
553+
},
554+
"example": {
555+
"json": "{\"name\": \"jq\", \"version\": \"1.7\"}",
556+
"query": ".name",
557+
"options": [
558+
"-r"
559+
]
479560
}
480561
}
481562
}
@@ -508,7 +589,19 @@
508589
"tags": [
509590
"Snippets"
510591
],
511-
"parameters": [],
592+
"parameters": [
593+
{
594+
"in": "path",
595+
"name": "slug",
596+
"schema": {
597+
"type": "string",
598+
"description": "Unique snippet identifier"
599+
},
600+
"required": true,
601+
"description": "Unique snippet identifier",
602+
"example": "slug"
603+
}
604+
],
512605
"responses": {
513606
"200": {
514607
"description": "Successful response",
@@ -530,4 +623,4 @@
530623
}
531624
}
532625
}
533-
}
626+
}

scripts/add-openapi-examples.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Post-process the generated OpenAPI spec to add examples for jq API parameters.
4+
* Run this after `next-openapi-gen generate`.
5+
*/
6+
7+
const fs = require('fs');
8+
const path = require('path');
9+
10+
const OPENAPI_PATH = path.join(__dirname, '../public/openapi.json');
11+
12+
// Example values that users can run immediately
13+
const GET_EXAMPLES = {
14+
json: '{"name": "jq", "version": "1.7"}',
15+
query: '.name',
16+
options: '-r,-c', // comma-separated string for query params
17+
};
18+
19+
const POST_EXAMPLES = {
20+
json: '{"name": "jq", "version": "1.7"}',
21+
query: '.name',
22+
options: ['-r', '-c'], // array for request body
23+
};
24+
25+
function addExamples() {
26+
const spec = JSON.parse(fs.readFileSync(OPENAPI_PATH, 'utf8'));
27+
28+
// Add examples to GET /jq query parameters
29+
const jqGetPath = spec.paths?.['/jq']?.get;
30+
if (jqGetPath?.parameters) {
31+
for (const param of jqGetPath.parameters) {
32+
if (param.name && GET_EXAMPLES[param.name]) {
33+
param.schema = param.schema || {};
34+
param.schema.example = GET_EXAMPLES[param.name];
35+
}
36+
}
37+
}
38+
39+
// Add examples to POST /jq request body schema
40+
const jqPostPath = spec.paths?.['/jq']?.post;
41+
const postBodySchema = jqPostPath?.requestBody?.content?.['application/json']?.schema;
42+
if (postBodySchema) {
43+
// Handle $ref - need to update the component schema directly
44+
if (postBodySchema.$ref) {
45+
const schemaName = postBodySchema.$ref.split('/').pop();
46+
const componentSchema = spec.components?.schemas?.[schemaName];
47+
if (componentSchema?.properties) {
48+
addExamplesToProperties(componentSchema.properties, POST_EXAMPLES);
49+
}
50+
} else if (postBodySchema.properties) {
51+
addExamplesToProperties(postBodySchema.properties, POST_EXAMPLES);
52+
}
53+
}
54+
55+
// Also update the JqQueryParamsSchema if it exists in components
56+
const queryParamsSchema = spec.components?.schemas?.JqQueryParamsSchema;
57+
if (queryParamsSchema?.properties) {
58+
addExamplesToProperties(queryParamsSchema.properties, GET_EXAMPLES);
59+
}
60+
61+
// Add example to POST /snippets request body (exclude http field)
62+
const snippetsPostPath = spec.paths?.['/snippets']?.post;
63+
const snippetsBodyContent = snippetsPostPath?.requestBody?.content?.['application/json'];
64+
if (snippetsBodyContent) {
65+
snippetsBodyContent.example = {
66+
json: '{"name": "jq", "version": "1.7"}',
67+
query: '.name',
68+
options: ['-r'],
69+
};
70+
}
71+
72+
fs.writeFileSync(OPENAPI_PATH, JSON.stringify(spec, null, 2) + '\n');
73+
console.log('Added examples to OpenAPI spec');
74+
}
75+
76+
function addExamplesToProperties(properties, examples) {
77+
for (const [name, prop] of Object.entries(properties)) {
78+
if (examples[name] && !prop.example) {
79+
prop.example = examples[name];
80+
}
81+
}
82+
}
83+
84+
addExamples();

src/app/api/jq/route.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import { ZodError } from 'zod';
22
import * as Sentry from '@sentry/nextjs';
3-
import { JqRequestSchema, JqResponse, JqError } from '@/schemas/api';
3+
import { JqRequestSchema, JqQueryParamsSchema, JqResponse, JqError } from '@/schemas/api';
44
import { runJqWithTimeout, TimeoutError } from '@/workers/server';
55

66
const TIMEOUT_MS = 5000; // 5 seconds
77

8-
function jsonResponse<T>(data: T, status: number): Response {
9-
return Response.json(data, { status });
8+
const CORS_HEADERS = {
9+
'Access-Control-Allow-Origin': '*',
10+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
11+
'Access-Control-Allow-Headers': 'Content-Type',
12+
};
13+
14+
function jsonResponse<T>(data: T, status: number, headers?: Record<string, string>): Response {
15+
return Response.json(data, {
16+
status,
17+
headers: { ...CORS_HEADERS, ...headers },
18+
});
1019
}
1120

1221
function handleError(e: unknown): Response {
@@ -31,9 +40,20 @@ function handleError(e: unknown): Response {
3140
return jsonResponse(error, 500);
3241
}
3342

43+
/**
44+
* Handle CORS preflight requests
45+
*/
46+
export async function OPTIONS() {
47+
return new Response(null, {
48+
status: 204,
49+
headers: CORS_HEADERS,
50+
});
51+
}
52+
3453
/**
3554
* Execute a jq query via query parameters
36-
* @description Execute a jq query against JSON input via query parameters
55+
* @description Execute a jq query against JSON input via query parameters. Great for simple queries and quick testing.
56+
* @params JqQueryParamsSchema
3757
* @response 200:JqResponseSchema
3858
* @response 400:JqErrorSchema
3959
* @response 408:JqErrorSchema
@@ -59,23 +79,27 @@ export async function GET(request: Request) {
5979

6080
const validated = JqRequestSchema.parse({ json, query, options });
6181

82+
const startTime = performance.now();
6283
const result = await runJqWithTimeout(
6384
validated.json,
6485
validated.query,
6586
validated.options ?? undefined,
6687
TIMEOUT_MS
6788
);
89+
const executionTime = Math.round(performance.now() - startTime);
6890

6991
const response: JqResponse = { result };
70-
return jsonResponse(response, 200);
92+
return jsonResponse(response, 200, {
93+
'X-Execution-Time': `${executionTime}ms`,
94+
});
7195
} catch (e: unknown) {
7296
return handleError(e);
7397
}
7498
}
7599

76100
/**
77101
* Execute a jq query
78-
* @description Execute a jq query against JSON input
102+
* @description Execute a jq query against JSON input. Use this for complex queries or large JSON payloads.
79103
* @body JqRequestSchema
80104
* @response 200:JqResponseSchema
81105
* @response 400:JqErrorSchema
@@ -88,15 +112,19 @@ export async function POST(request: Request) {
88112
const body = await request.json();
89113
const { json, query, options } = JqRequestSchema.parse(body);
90114

115+
const startTime = performance.now();
91116
const result = await runJqWithTimeout(
92117
json,
93118
query,
94119
options ?? undefined,
95120
TIMEOUT_MS
96121
);
122+
const executionTime = Math.round(performance.now() - startTime);
97123

98124
const response: JqResponse = { result };
99-
return jsonResponse(response, 200);
125+
return jsonResponse(response, 200, {
126+
'X-Execution-Time': `${executionTime}ms`,
127+
});
100128
} catch (e: unknown) {
101129
return handleError(e);
102130
}

0 commit comments

Comments
 (0)