
Nextjs Refine REST API (Backend)
- Atul
- Programming
- December 20, 2024
Table of Contents
In previous post I have consumed the REST API for the frontend but now I want create the backend so that I get understanding of the backend it requires for Refine frontend.
API Provider Simple REST
The Simple REST data provider is a package that provides an implementation for working with REST APIs that conform to a standard API design
. It is built on the foundation of the json-server package.
The Standard API Design.
This is the mapping of functions in Data provider to REST
HTTP Methods used by these functions:
GET /posts
GET /posts/:id
POST /posts
PUT /posts/:id
PATCH /posts/:id
DELETE /posts/:id
- app/api/posts/route.ts
- GET
- POST
- app/api/posts/[id]/route.ts
- GET
- PUT
- PATCH
- DELETE
Params
Range
start
end
limit
GET /posts?_start=10&_end=20
GET /posts?_start=10&_limit=10
Paginate
page
per_page
(default = 10)
GET /posts?_page=1&_per_page=25
Sort
_sort=f1,f2
GET /posts?_sort=id,-views
Conditions
→
==
lt
→<
lte
→<=
gt
→>
gte
→>=
ne
→!=
GET /posts?views_gt=9000
Embed
GET /posts?_embed=comments
GET /comments?_embed=post
Delete
DELETE /posts/1
DELETE /posts/1?_dependent=comments
Prisma integration:
Here is my blog post for this.
yarn add prisma -D
Add all the things added in blog post
npx prisma migrate dev --name init
create a file src/lib/db.ts
// src/lib/db.ts
import { PrismaClient } from "@prisma/client";
declare global {
var prisma: PrismaClient | undefined;
}
const prisma =
globalThis.prisma ||
new PrismaClient({
// Add data proxy configuration if needed
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
});
if (process.env.NODE_ENV !== "production") {
globalThis.prisma = prisma;
}
export default prisma;
Main task is to export prisma from here.
Create Backend API routes
- app/api/[tableName]/route.ts
- app/api/[tableName]/[id]/route.ts
This is for generic
Frontend routes
src/app/distros
├── create
│ └── page.tsx
├── edit
│ └── [id]
│ └── page.tsx
├── layout.tsx
├── page.tsx
└── show
└── [id]
└── page.tsx
I did all the same but you need to study about Antd
Creating Distro with an image upload:
This is the blog where I found out about the image upload.
Similar to this: https://api.fake-rest.refine.dev/media/upload
I need an API to media upload.
This end-point should be Content-type: multipart/form-data and Form Data: file: binary.
And response should be like:
{
"url": "https://example.com/uploaded-file.jpeg"
}
This data is sent to the API when the form is submitted. [POST] https://api.fake-rest.refine.dev/posts
{
"title": "Test",
"image": [
{
"uid": "rc-upload-1620630541327-7",
"name": "greg-bulla-6RD0mcpY8f8-unsplash.jpg",
"url": "https://refine.ams3.digitaloceanspaces.com/78c82c0b2203e670d77372f4c20fc0e2",
"type": "image/jpeg",
"size": 70922,
"percent": 100,
"status": "done"
}
]
}
CAUTION
The following data are required for the Antd Upload component and all should be saved.
So I need to save all these fields for an Image!!
Well let’s get started with it.
Create API endpoint
Create a folder /api/media/upload
Then create a route.ts file
This endpoint needs to be a POST
import { NextRequest } from "next/server";
export async function POST(request: NextRequest) {
try {
} catch (error) {}
}
This is how it looks for now.
Add Uploadthing to project:
Here is my blog posts about Uploadthing
Yeah yeah not much in there!! LOL
yarn add uploadthing
Now get yourself a UPLOADTHING_TOKEN, that you can check in my blog for sure ;)
Save that token to .env file.
// Handle file upload if present
const imageFile = formData.get("image") as File;
if (imageFile) {
const utapi = new UTApi();
// Upload file to UploadThing
const uploadResult = await utapi.uploadFiles([imageFile]);
if (!uploadResult || !uploadResult[0]?.data?.url) {
throw new Error("Image upload failed");
}
// Get the uploaded image URL from UploadThing
data.imageUrl = uploadResult[0].data.url;
if (isDevelopment) {
console.log("Updated data with image_url:", data);
}
}
This is the code block I used in some other project. Let’s see if this is useful.
That codeblock is converted to this:
import { NextRequest, NextResponse } from "next/server";
import { UTApi } from "uploadthing/server";
export async function POST(request: NextRequest) {
try {
// Handle file upload if present
const formData = await request.formData();
const imageFile = formData.get("image") as File;
if (imageFile) {
const utapi = new UTApi();
// Upload file to UploadThing
const uploadResult = await utapi.uploadFiles([imageFile]);
if (!uploadResult || !uploadResult[0]?.data?.url) {
throw new Error("Image upload failed");
}
return NextResponse.json({ url: uploadResult });
}
} catch (error) {}
}
But first let’s fix frontend issue:
Error: TypeError: url.split is not a function
This occured while adding an image to the Upload section of form.
Well I just made sure that my code was similar to the documentation one.
"use client";
import React from "react";
import { Create, useForm, getValueFromEvent } from "@refinedev/antd";
import { Form, Input, InputNumber, Upload } from "antd";
import { useApiUrl } from "@refinedev/core";
export default function DistroCreate() {
const { formProps, saveButtonProps } = useForm();
const apiUrl = useApiUrl();
return (
<Create saveButtonProps={saveButtonProps}>
<Form {...formProps} layout="vertical">
<Form.Item
label="Name"
name={["name"]}
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="Version"
name={["version"]}
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="Release Year"
name={["releaseYear"]}
rules={[
{
required: true,
},
{
type: "number",
},
]}
>
<InputNumber />
</Form.Item>
<Form.Item
label="Description"
name={["description"]}
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="Website"
name={["website"]}
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item label="Logo">
<Form.Item
name="logo"
getValueFromEvent={getValueFromEvent}
noStyle
rules={[
{
required: false,
},
]}
>
<Upload.Dragger
listType="picture"
action={`${apiUrl}/media/upload`}
maxCount={5}
multiple
>
<p className="ant-upload-text">Drag & drop a file in this area</p>
</Upload.Dragger>
</Form.Item>
</Form.Item>
</Form>
</Create>
);
}
Now the API work needs to be done.
Updated API
import { NextRequest, NextResponse } from "next/server";
import { UTApi } from "uploadthing/server";
export async function POST(request: NextRequest) {
try {
const formData = await request.formData();
const file = formData.get("file") as File;
if (!file) {
return NextResponse.json({ error: "No file provided" }, { status: 400 });
}
const utapi = new UTApi();
const uploadResult = await utapi.uploadFiles([file]);
if (!uploadResult || !uploadResult[0]?.data?.url) {
throw new Error("Image upload failed");
}
// Format response to match Antd Upload component requirements
const response = {
url: uploadResult[0].data.url,
uid: `upload-${Date.now()}`, // Generate unique ID
name: file.name,
type: file.type,
size: file.size,
status: "done",
percent: 100,
};
return NextResponse.json(response);
} catch (error) {
console.error("Upload error:", error);
return NextResponse.json({ error: "Upload failed" }, { status: 500 });
}
}
Now the image is being saved.
Let’s see if it saved correctly or not.
NOPE!
{
"error": "Error creating record, PrismaClientValidationError: \nInvalid `prisma.distro.create()` invocation:\n\n{\n data: {\n name: \"Test\",\n version: \"Test\",\n releaseYear: 333,\n description: \"Test\",\n website: \"Test\",\n logo: [\n {\n uid: \"rc-upload-1735043056500-7\",\n lastModified: 1733672722670,\n name: \"Admin.jpg\",\n size: 10253,\n type: \"image/jpeg\",\n percent: 100,\n originFileObj: {\n uid: \"rc-upload-1735043056500-7\"\n },\n status: \"done\",\n response: {\n url: \"https://utfs.io/f/MwCVYPRyYlZW70kIP6JKzhMJWwHOEN298rA1LnTqjIK5xm0B\",\n uid: \"upload-1735043349057\",\n name: \"Admin.jpg\",\n type: \"image/jpeg\",\n size: 10253,\n status: \"done\",\n percent: 100\n },\n xhr: {},\n thumbUrl: \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAFHklEQVR4Xu3dsUlsURQFUAYGLEEQM8HAwMBcsAlDU8EKzIwswgKMBRswGQRNbEK0ATMjK9ADbpHZzIofL/mc7b1nr5n5i9vzve2b59ed1cPZ9dXF6vhuefq0+/J5f3D0ePixv9w6eX+79Ny/z6bOx0JA/AHwB/D7P4AC4oR0Q/jhBiUgAiIgAmKH2NQdIt2hnSBOECeIE8QJ4gT5XUvrBHGCOEGcIE4QJ4gTBHSC3j+HblcsVyxXLFcsVyxXLFcsVyxXLFcsH6b0YdL//DCtHcQOYgexg9hB7CB2EDuIHcQOYgexg9hBfKPRNzrX5ButlnRLuiXdkm5Jt6Rb0i3plnRLuiXdkm5Jt6Rb0i3pfnfL746t/++uabG0WFosLZYWS4ulxdJiabG0WFosLZYWS4ulxdJiabG0WFos/32Clqy6JVPzGuDqAU5/e3d6X0AEREA4CAfhIByEg3AQDsJBOAgH4SAchINwEA7CQTiIlqy6JVPzGuDqAZ4cI30uIAIiIByEg3AQDsJBOAgH4SAchINwEA7CQTgIB+EgHERLVt2SqXkNcPUAp84xvS8gAiIgHISDcBAOwkE4CAfhIByEg3AQDsJBOAgH4SAcREtW3ZKpeQ1w9QBPjpE+FxABERAOwkE4CAfhIByEg3AQDsJBOAgH4SAchINwEA6iJatuydS8Brh6gFPnmN4XEAEREA7CQTgIB+EgHISDcBAOwkE4CAfhIByEg3AQDqIlq27J1LwGuHqAJ8dInwuIgAgIB+EgHISDcBAOwkE4CAfhIByEg3AQDsJBOAgH0ZJVt2RqXgNcPcCpc0zvC4iACAgH4SAchINwEA7CQTgIB+EgHISDcBAOwkE4CAfRklW3ZGpeA1w9wJNjpM8FREAEhINwEA7CQTgIB+EgHISDcBAOwkE4CAfhIByEg2jJqlsyNa8Brh7g1Dmm9wVEQASEg3AQDsJBOAgH4SAchINwEA7CQTgIB+EgHISDaMmqWzI1rwGuHuDJMdLnAiIgAsJBOAgH4SAchINwEA7CQTgIB+EgHISDcBAOwkG0ZNUtmZrXAFcPcOoc0/sCIiACwkE4CAfhIByEg3AQDsJBOAgH4SAchINwEA7CQbRk1S2ZmtcAVw/w5BjpcwEREAHhIByEg3AQDsJBOAgH4SAchINwEA7CQTgIB+EgWrLqlkzNa4CrBzh1jul9AREQAeEgHISDcBAOwkE4CAfhIByEg3AQDsJBOAgH4SBasuqWTM1rgKsHeHKM9LmACIiAcBAOwkE4CAfhIByEg3AQDsJBOAgH4SAchINwEC1ZdUum5jXA1QOcOsf0voAIiIBwEA7CQTgIB+EgHISDcBAOwkE4CAfhIByEg3AQLVl1S6bmNcDVAzw5RvpcQAREQDgIB+EgHISDcBAOwkE4CAfhIByEg3AQDsJBOIiWrLolU/Ma4OoBTp1jel9ABERAOAgH4SAchINwEA7CQTgIB+EgHISDcBAOwkE4iJasuiVT8xrg6gGeHCN9LiACIiAchINwEA7CQTgIB+EgHISDcBAOwkE4CAfhIBxES1bdkql5DXD1AKfOMb0vIAIiIByEg3AQDsJBOAgH4SAchINwEA7CQTgIB+EgHERLVt2SqXkNcPUAT46RPhcQAREQDsJBOAgH4SAchINwEA7CQTgIB+EgHISDcBAOoiWrbsnUvAa4eoBT55jeFxABERAOwkE4CAfhIByEg3AQDsJBOAgH4SAchINwEA6iJatuyb4AXOyakmpjCR4AAAAASUVORK5CYII=\"\n }\n ]\n \n }\n}\n\nArgument `logo`: Invalid value provided. Expected String or Null, provided (Object)."
}
Well now I need to change the POST to accept this type of data for record creation.
This is the root cause.
So we need to change a bit now. Not a bit but a lot.
We need to store all the meta data the AntDesign requires, for that logo should be JSON field.
Update Prisma schema
Update distros logo to be Json.
Error!!
So till now we were using sqlite for database now we need to change it.
datasource db {
provider = "postgres"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Distro {
id String @id @default(uuid())
name String
version String
releaseYear Int
description String
website String
logo Json?
}
Updated schema
Update Database Url:
Using Prisma postgres, yeah yeah I’ve blogged about it here
Get your URL for .env file and then update that.
Delete the old migration files the run :
npx prisma migrate dev --name update_logo_to_json
This is taking time, hope it works.
Great to see this: Your database is now in sync with your schema.
Great!!!
Update POST to take new logo data:
export async function POST(
request: NextRequest,
{ params }: { params: { tableName: string } }
) {
try {
const { tableName } = params;
if (!modelMap[tableName]) {
return NextResponse.json(
{ error: "Invalid table name" },
{ status: 400 }
);
}
const data = await request.json();
const modelConfig = modelMap[tableName];
// Deep clone the data to avoid reference issues
let processedData = { ...data };
// Log the complete data structure
console.log("Full processedData:", JSON.stringify(processedData, null, 2));
// Or if you want to see it as entries but with full detail:
const detailedEntries = Object.entries(processedData).map(
([key, value]) => {
return [key, JSON.stringify(value, null, 2)];
}
);
console.log("Detailed entries:", detailedEntries);
// Rest of your handler code...
if (
processedData.logo &&
Array.isArray(processedData.logo) &&
processedData.logo.length > 0
) {
const logoData = processedData.logo[0];
processedData.logo = {
uid: logoData.uid,
name: logoData.name,
url: logoData.response?.url || logoData.url,
type: logoData.type,
size: logoData.size,
status: logoData.status,
};
}
// Log the processed data after transformation
console.log("After processing:", JSON.stringify(processedData, null, 2));
// Continue with your existing code...
const invalidKeys = Object.keys(processedData).filter(
(key) => !modelConfig.attributes.includes(key)
);
if (invalidKeys.length > 0) {
return NextResponse.json(
{ error: `Invalid attributes: ${invalidKeys.join(", ")}` },
{ status: 400 }
);
}
const result = await modelConfig.model.create({
data: processedData,
});
return NextResponse.json(result, { status: 201 });
} catch (error) {
console.error("Error details:", error);
if (error instanceof Error) {
if (error.message.includes("Unique constraint")) {
return NextResponse.json(
{ error: "Record already exists" },
{ status: 409 }
);
}
}
return NextResponse.json(
{ error: `Error creating record, ${error}` },
{ status: 500 }
);
}
}
Image is not being shown!!
I noticed that image from get is inside the an array
I was able to show this image but like this:
<Table.Column
dataIndex={["logo"]}
title="Logo"
render={(value: any) => {
console.log("value logo", value);
return <ImageField style={{ maxWidth: "100px" }} value={value[0].url} />;
}}
/>
Here this is expecting data like logo: “url to the logo”
But I think I may have got too overboard.
I guess it is time to undone what I have just done!
- Downdate the schema to string again.
- Don’t save the json data in logo field just save the url.
- List view to take logo as simple way.
So, the image is visible now
But the edit page is having the same issue of url.split.
The edit page also sending data like the one in create page now I need to update the /[id]/route.ts
Replicated the create API with update one.
Now the update is also working.
Sorry for the deviation above, I guess this is what happens when you wander around.
Repo link